file-toolkit-mcp 1.0.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.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 统一的结果输出格式。
3
+ *
4
+ * 每个工具都返回 Markdown 摘要,而不是裸 JSON。原因:
5
+ * 1. 模型读 Markdown 表格比读嵌套 JSON 更不容易看错行列
6
+ * 2. 模型可以直接把这段文本转述给用户,不需要自己重新组织
7
+ * 3. 附带的结构化数据放在代码块里,模型需要精确数值时可直接取用
8
+ */
9
+
10
+ /** 人类可读的体积格式 */
11
+ export function formatBytes(bytes) {
12
+ if (!Number.isFinite(bytes) || bytes < 0) return '未知';
13
+ if (bytes < 1024) return `${bytes} B`;
14
+ const units = ['KB', 'MB', 'GB', 'TB'];
15
+ let value = bytes / 1024;
16
+ let idx = 0;
17
+ while (value >= 1024 && idx < units.length - 1) {
18
+ value /= 1024;
19
+ idx += 1;
20
+ }
21
+ return `${value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)} ${units[idx]}`;
22
+ }
23
+
24
+ /** 压缩率展示:负数表示变小了 */
25
+ export function formatDelta(before, after) {
26
+ if (!Number.isFinite(before) || before <= 0 || !Number.isFinite(after)) return '—';
27
+ const ratio = (1 - after / before) * 100;
28
+ if (ratio >= 0) return `↓ ${ratio.toFixed(1)}%`;
29
+ return `↑ ${Math.abs(ratio).toFixed(1)}%`;
30
+ }
31
+
32
+ /**
33
+ * 生成标准结果对象。
34
+ *
35
+ * @param {object} params
36
+ * @param {string} params.title 一句话结论
37
+ * @param {string[]} [params.table] Markdown 表格的行(含表头与分隔行)
38
+ * @param {string[]} [params.notes] 附加说明
39
+ * @param {object} [params.data] 结构化数据
40
+ * @param {string} [params.outputDir] 输出目录
41
+ */
42
+ export function buildResult({ title, table = [], notes = [], data = null, outputDir = null }) {
43
+ const lines = [title];
44
+
45
+ if (table.length > 0) {
46
+ lines.push('', ...table);
47
+ }
48
+
49
+ if (outputDir) {
50
+ lines.push('', `输出目录:\`${outputDir}\``);
51
+ }
52
+
53
+ for (const note of notes) {
54
+ if (note) lines.push('', note);
55
+ }
56
+
57
+ if (data) {
58
+ lines.push('', '```json', JSON.stringify(data, null, 2), '```');
59
+ }
60
+
61
+ return lines.join('\n');
62
+ }
63
+
64
+ /** 组装 Markdown 表格 */
65
+ export function mdTable(headers, rows) {
66
+ const escapeCell = (v) => String(v ?? '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
67
+ const head = `| ${headers.map(escapeCell).join(' | ')} |`;
68
+ const sep = `| ${headers.map(() => '---').join(' | ')} |`;
69
+ const body = rows.map((r) => `| ${r.map(escapeCell).join(' | ')} |`);
70
+ return [head, sep, ...body];
71
+ }
72
+
73
+ /**
74
+ * 统计成功/失败,生成「N 个成功,M 个失败」这样的结论句。
75
+ */
76
+ export function summarize(action, results) {
77
+ const ok = results.filter((r) => r.ok).length;
78
+ const failed = results.length - ok;
79
+ if (failed === 0) {
80
+ return `${action}完成:${ok} 个文件全部成功。`;
81
+ }
82
+ if (ok === 0) {
83
+ return `${action}失败:${failed} 个文件都没有处理成功。`;
84
+ }
85
+ return `${action}完成:${ok} 个成功,${failed} 个失败。`;
86
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * 文本格式转换:Markdown <-> HTML。
3
+ * 走 marked(MIT)与 turndown(MIT),都是纯 JS,无系统依赖。
4
+ */
5
+
6
+ import { marked } from 'marked';
7
+ import TurndownService from 'turndown';
8
+
9
+ const PAGE_TEMPLATE = (title, body) => `<!DOCTYPE html>
10
+ <html lang="zh-CN">
11
+ <head>
12
+ <meta charset="utf-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1">
14
+ <title>${escapeHtml(title)}</title>
15
+ <style>
16
+ body { max-width: 46rem; margin: 2rem auto; padding: 0 1rem;
17
+ font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
18
+ line-height: 1.75; color: #24292f; }
19
+ pre { background: #f6f8fa; padding: 1rem; overflow: auto; border-radius: 6px; }
20
+ code { background: #f6f8fa; padding: .15em .35em; border-radius: 4px; font-size: .92em; }
21
+ pre code { background: none; padding: 0; }
22
+ table { border-collapse: collapse; width: 100%; }
23
+ th, td { border: 1px solid #d0d7de; padding: .5rem .75rem; text-align: left; }
24
+ th { background: #f6f8fa; }
25
+ img { max-width: 100%; }
26
+ blockquote { margin: 0; padding: 0 1rem; color: #57606a; border-left: .25rem solid #d0d7de; }
27
+ </style>
28
+ </head>
29
+ <body>
30
+ ${body}
31
+ </body>
32
+ </html>
33
+ `;
34
+
35
+ function escapeHtml(text) {
36
+ return String(text ?? '')
37
+ .replace(/&/g, '&amp;')
38
+ .replace(/</g, '&lt;')
39
+ .replace(/>/g, '&gt;')
40
+ .replace(/"/g, '&quot;');
41
+ }
42
+
43
+ /**
44
+ * Markdown -> HTML
45
+ * @param {string} markdown
46
+ * @param {{ fullPage?: boolean, title?: string }} [options]
47
+ */
48
+ export function markdownToHtml(markdown, options = {}) {
49
+ const { fullPage = true, title = 'Document' } = options;
50
+ const source = String(markdown ?? '');
51
+ const body = marked.parse(source, { gfm: true, breaks: false });
52
+ return fullPage ? PAGE_TEMPLATE(title, body) : body;
53
+ }
54
+
55
+ /**
56
+ * HTML -> Markdown
57
+ */
58
+ export function htmlToMarkdown(html) {
59
+ const service = new TurndownService({
60
+ headingStyle: 'atx',
61
+ codeBlockStyle: 'fenced',
62
+ bulletListMarker: '-',
63
+ emDelimiter: '*',
64
+ });
65
+
66
+ // 保留表格结构(turndown 默认把 table 丢掉)
67
+ service.keep(['table', 'thead', 'tbody', 'tr', 'th', 'td']);
68
+
69
+ return service.turndown(String(html ?? ''));
70
+ }
71
+
72
+ /**
73
+ * 从 HTML 里猜一个标题,用作输出的 PDF/HTML 标题
74
+ */
75
+ export function guessTitle(text) {
76
+ const match = String(text ?? '').match(/^\s*#\s+(.+)$/m);
77
+ if (match) return match[1].trim().slice(0, 120);
78
+ const firstLine = String(text ?? '').split('\n').find((l) => l.trim() !== '');
79
+ return (firstLine ?? 'Document').trim().slice(0, 120) || 'Document';
80
+ }
package/src/server.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * File Toolkit MCP Server 入口。
3
+ *
4
+ * 传输方式:stdio(本地进程,由 WorkBuddy 拉起)。
5
+ * 注意:stdio 模式下 stdout 被 MCP 协议独占,
6
+ * 任何日志都必须走 stderr,否则会破坏协议。
7
+ */
8
+
9
+ import { readFile } from 'node:fs/promises';
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+
13
+ import { registerImageTools } from './tools/image-tools.js';
14
+ import { registerPdfTools } from './tools/pdf-tools.js';
15
+ import { registerDataTools } from './tools/data-tools.js';
16
+ import { registerFileTools } from './tools/file-tools.js';
17
+
18
+ const pkg = JSON.parse(
19
+ await readFile(new URL('../package.json', import.meta.url), 'utf8'),
20
+ );
21
+
22
+ const INSTRUCTIONS = `File Toolkit 是一个完全在本机运行的文件处理工具箱。
23
+
24
+ - 所有处理都在用户自己的电脑上完成,不会把文件上传到任何服务器。
25
+ - 默认不覆盖原文件:结果统一写到「第一个输入文件所在目录」下的 file-toolkit-output 文件夹;
26
+ 如果该目录里已有同名文件,会自动加序号,绝不覆盖。
27
+ - 工具既可以接收单个文件,也可以接收整个文件夹(会自动收集里面的文件并批量处理)。
28
+ - 批量处理时单个文件失败不会中断整批,结果里会分别列出成功和失败。
29
+ - batch_rename 默认只返回预览,必须先让用户确认,再用 confirm=true 真正执行;
30
+ 执行后可用 rename_undo 回滚。
31
+
32
+ 支持的图片格式:jpg / png / webp。支持处理 PDF、CSV、TSV、JSON、Markdown、HTML。`;
33
+
34
+ async function main() {
35
+ const server = new McpServer(
36
+ { name: pkg.name, version: pkg.version },
37
+ { instructions: INSTRUCTIONS },
38
+ );
39
+
40
+ registerImageTools(server);
41
+ registerPdfTools(server);
42
+ registerDataTools(server);
43
+ registerFileTools(server);
44
+
45
+ const transport = new StdioServerTransport();
46
+ await server.connect(transport);
47
+
48
+ // 这些错误不应该让已经建立的服务连接直接消失
49
+ process.on('uncaughtException', (error) => {
50
+ console.error('[file-toolkit] 未捕获异常:', error?.stack ?? error);
51
+ });
52
+ process.on('unhandledRejection', (reason) => {
53
+ console.error('[file-toolkit] 未处理的 Promise 拒绝:', reason);
54
+ });
55
+ }
56
+
57
+ main().catch((error) => {
58
+ console.error('[file-toolkit] 启动失败:', error?.stack ?? error);
59
+ process.exit(1);
60
+ });