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/README.md +64 -1
- package/SKILL.md +40 -0
- package/bin/agent-reader.js +1 -1
- package/openclaw-skill/SKILL.md +40 -0
- package/openclaw-skill/schema.json +197 -0
- package/package.json +1 -1
- package/src/cli/commands.js +22 -2
- package/src/core/assets.js +8 -0
- package/src/core/exporter.js +279 -17
- package/src/core/opener.js +4 -1
- package/src/core/renderer.js +7 -6
- package/src/core/themes/dark.css +322 -182
- package/src/mcp/server.js +12 -52
- package/src/mcp/toolSchemas.js +53 -0
- package/src/utils/pathGuard.js +62 -0
- package/src/utils/server.js +10 -4
- package/src/core/themes/print.css +0 -54
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: '
|
|
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
|
+
}
|
package/src/utils/server.js
CHANGED
|
@@ -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(
|
|
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
|
-
}
|