lightpdf-aipdf-mcp 0.1.31__py3-none-any.whl
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.
- lightpdf_aipdf_mcp/__init__.py +8 -0
- lightpdf_aipdf_mcp/common.py +181 -0
- lightpdf_aipdf_mcp/converter.py +298 -0
- lightpdf_aipdf_mcp/editor.py +524 -0
- lightpdf_aipdf_mcp/server.py +888 -0
- lightpdf_aipdf_mcp-0.1.31.dist-info/METADATA +208 -0
- lightpdf_aipdf_mcp-0.1.31.dist-info/RECORD +9 -0
- lightpdf_aipdf_mcp-0.1.31.dist-info/WHEEL +4 -0
- lightpdf_aipdf_mcp-0.1.31.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,888 @@
|
|
1
|
+
"""LightPDF AI助手 MCP Server模块"""
|
2
|
+
# 标准库导入
|
3
|
+
import asyncio
|
4
|
+
import os
|
5
|
+
from typing import List, Dict, Any, Callable, TypeVar, Optional, Union
|
6
|
+
|
7
|
+
# 第三方库导入
|
8
|
+
from dotenv import load_dotenv
|
9
|
+
|
10
|
+
# MCP相关导入
|
11
|
+
from mcp.server.lowlevel import Server, NotificationOptions
|
12
|
+
from mcp.server.models import InitializationOptions
|
13
|
+
import mcp.types as types
|
14
|
+
|
15
|
+
# 本地导入
|
16
|
+
from .common import BaseResult, Logger, FileHandler
|
17
|
+
from .converter import Converter, ConversionResult
|
18
|
+
from .editor import Editor, EditResult
|
19
|
+
|
20
|
+
# 加载环境变量
|
21
|
+
load_dotenv()
|
22
|
+
|
23
|
+
# 类型定义
|
24
|
+
T = TypeVar('T', bound=BaseResult)
|
25
|
+
ProcessFunc = Callable[[str], Any]
|
26
|
+
|
27
|
+
def generate_result_report(
|
28
|
+
results: List[BaseResult],
|
29
|
+
operation_name: str,
|
30
|
+
success_action: Optional[str] = None,
|
31
|
+
failed_action: Optional[str] = None
|
32
|
+
) -> str:
|
33
|
+
"""生成通用结果报告
|
34
|
+
|
35
|
+
Args:
|
36
|
+
results: 结果列表
|
37
|
+
operation_name: 操作名称
|
38
|
+
success_action: 成功动作描述
|
39
|
+
failed_action: 失败动作描述
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
str: 格式化的报告文本
|
43
|
+
"""
|
44
|
+
# 统计结果
|
45
|
+
success_count = sum(1 for r in results if r.success)
|
46
|
+
failed_count = len(results) - success_count
|
47
|
+
|
48
|
+
# 生成报告头部
|
49
|
+
report_lines = [
|
50
|
+
f"{operation_name}结果:共 {len(results)} 个文件" + (
|
51
|
+
f",成功 {success_count} 个" if success_count > 0 else ""
|
52
|
+
) + (
|
53
|
+
f",失败 {failed_count} 个" if failed_count > 0 else ""
|
54
|
+
),
|
55
|
+
""
|
56
|
+
]
|
57
|
+
|
58
|
+
# 添加成功的文件信息
|
59
|
+
if success_count > 0 and success_action:
|
60
|
+
report_lines.extend([f"[成功] {success_action}的文件:", ""])
|
61
|
+
for i, result in enumerate(results):
|
62
|
+
if result.success:
|
63
|
+
report_lines.extend([
|
64
|
+
f"[{i+1}] {result.file_path}",
|
65
|
+
f"- 在线下载地址: {result.download_url}",
|
66
|
+
""
|
67
|
+
])
|
68
|
+
|
69
|
+
# 添加失败的文件信息
|
70
|
+
if failed_count > 0 and failed_action:
|
71
|
+
report_lines.extend([f"[失败] {failed_action}的文件:", ""])
|
72
|
+
for i, result in enumerate(results):
|
73
|
+
if not result.success:
|
74
|
+
report_lines.extend([
|
75
|
+
f"[{i+1}] {result.file_path}",
|
76
|
+
f"- 错误: {result.error_message}",
|
77
|
+
""
|
78
|
+
])
|
79
|
+
|
80
|
+
return "\n".join(report_lines)
|
81
|
+
|
82
|
+
async def process_batch_files(
|
83
|
+
file_objects: List[Dict[str, str]],
|
84
|
+
logger: Logger,
|
85
|
+
process_func: Callable[[str, Optional[str]], T],
|
86
|
+
operation_desc: Optional[str] = None
|
87
|
+
) -> List[T]:
|
88
|
+
"""通用批处理文件函数
|
89
|
+
|
90
|
+
Args:
|
91
|
+
file_objects: 文件对象列表,每个对象包含path和可选的password
|
92
|
+
logger: 日志记录器
|
93
|
+
process_func: 处理单个文件的异步函数,接收file_path和password参数
|
94
|
+
operation_desc: 操作描述,用于日志记录
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
List[T]: 处理结果列表
|
98
|
+
"""
|
99
|
+
if len(file_objects) > 1 and operation_desc:
|
100
|
+
await logger.log("info", f"开始批量{operation_desc},共 {len(file_objects)} 个文件")
|
101
|
+
|
102
|
+
# 并发处理文件,限制并发数为5
|
103
|
+
semaphore = asyncio.Semaphore(5)
|
104
|
+
|
105
|
+
async def process_with_semaphore(file_obj: Dict[str, str]) -> T:
|
106
|
+
async with semaphore:
|
107
|
+
file_path = file_obj["path"]
|
108
|
+
password = file_obj.get("password")
|
109
|
+
return await process_func(file_path, password)
|
110
|
+
|
111
|
+
# 创建任务列表
|
112
|
+
tasks = [process_with_semaphore(file_obj) for file_obj in file_objects]
|
113
|
+
return await asyncio.gather(*tasks)
|
114
|
+
else:
|
115
|
+
# 单文件处理
|
116
|
+
file_path = file_objects[0]["path"]
|
117
|
+
password = file_objects[0].get("password")
|
118
|
+
return [await process_func(file_path, password)]
|
119
|
+
|
120
|
+
async def process_conversion_file(
|
121
|
+
file_path: str,
|
122
|
+
format: str,
|
123
|
+
converter: Converter,
|
124
|
+
extra_params: Optional[Dict[str, Any]] = None,
|
125
|
+
password: Optional[str] = None
|
126
|
+
) -> ConversionResult:
|
127
|
+
"""处理单个文件转换"""
|
128
|
+
is_page_numbering = format == "number-pdf"
|
129
|
+
|
130
|
+
if is_page_numbering and extra_params:
|
131
|
+
# 对于添加页码,使用add_page_numbers方法
|
132
|
+
return await converter.add_page_numbers(
|
133
|
+
file_path,
|
134
|
+
extra_params.get("start_num", 1),
|
135
|
+
extra_params.get("position", "5"),
|
136
|
+
extra_params.get("margin", 30),
|
137
|
+
password
|
138
|
+
)
|
139
|
+
else:
|
140
|
+
# 对于其他操作,使用convert_file方法
|
141
|
+
return await converter.convert_file(file_path, format, extra_params, password)
|
142
|
+
|
143
|
+
async def process_edit_file(
|
144
|
+
file_path: str,
|
145
|
+
edit_type: str,
|
146
|
+
editor: Editor,
|
147
|
+
extra_params: Dict[str, Any] = None,
|
148
|
+
password: Optional[str] = None
|
149
|
+
) -> EditResult:
|
150
|
+
"""处理单个文件编辑"""
|
151
|
+
if edit_type == "decrypt":
|
152
|
+
return await editor.decrypt_pdf(file_path, password)
|
153
|
+
elif edit_type == "add_watermark":
|
154
|
+
return await editor.add_watermark(
|
155
|
+
file_path=file_path,
|
156
|
+
text=extra_params.get("text", "水印"),
|
157
|
+
position=extra_params.get("position", "center"),
|
158
|
+
opacity=extra_params.get("opacity", 0.5),
|
159
|
+
deg=extra_params.get("deg", "0"),
|
160
|
+
range=extra_params.get("range", ""),
|
161
|
+
layout=extra_params.get("layout", "under"),
|
162
|
+
font_family=extra_params.get("font_family"),
|
163
|
+
font_size=extra_params.get("font_size"),
|
164
|
+
font_color=extra_params.get("font_color"),
|
165
|
+
password=password
|
166
|
+
)
|
167
|
+
elif edit_type == "encrypt":
|
168
|
+
return await editor.encrypt_pdf(
|
169
|
+
file_path=file_path,
|
170
|
+
password=extra_params.get("password", ""),
|
171
|
+
original_password=password
|
172
|
+
)
|
173
|
+
elif edit_type == "compress":
|
174
|
+
return await editor.compress_pdf(
|
175
|
+
file_path=file_path,
|
176
|
+
image_quantity=extra_params.get("image_quantity", 60),
|
177
|
+
password=password
|
178
|
+
)
|
179
|
+
elif edit_type == "split":
|
180
|
+
return await editor.split_pdf(
|
181
|
+
file_path=file_path,
|
182
|
+
pages=extra_params.get("pages", ""),
|
183
|
+
password=password,
|
184
|
+
split_type=extra_params.get("split_type", "page"),
|
185
|
+
merge_all=extra_params.get("merge_all", 1)
|
186
|
+
)
|
187
|
+
elif edit_type == "merge":
|
188
|
+
# 对于合并操作,我们需要特殊处理,因为它需要处理多个文件
|
189
|
+
return EditResult(
|
190
|
+
success=False,
|
191
|
+
file_path=file_path,
|
192
|
+
error_message="合并操作需要使用特殊处理流程"
|
193
|
+
)
|
194
|
+
elif edit_type == "rotate":
|
195
|
+
return await editor.rotate_pdf(
|
196
|
+
file_path=file_path,
|
197
|
+
angle=extra_params.get("angle", 90),
|
198
|
+
pages=extra_params.get("pages", ""),
|
199
|
+
password=password
|
200
|
+
)
|
201
|
+
else:
|
202
|
+
return EditResult(
|
203
|
+
success=False,
|
204
|
+
file_path=file_path,
|
205
|
+
error_message=f"不支持的编辑类型: {edit_type}"
|
206
|
+
)
|
207
|
+
|
208
|
+
async def process_tool_call(
|
209
|
+
logger: Logger,
|
210
|
+
file_objects: List[Dict[str, str]],
|
211
|
+
operation_config: Dict[str, Any]
|
212
|
+
) -> types.TextContent:
|
213
|
+
"""通用工具调用处理函数
|
214
|
+
|
215
|
+
Args:
|
216
|
+
logger: 日志记录器
|
217
|
+
file_objects: 文件对象列表,每个对象包含path和可选的password
|
218
|
+
operation_config: 操作配置,包括操作类型、格式、参数等
|
219
|
+
|
220
|
+
Returns:
|
221
|
+
types.TextContent: 包含处理结果的文本内容
|
222
|
+
"""
|
223
|
+
file_handler = FileHandler(logger)
|
224
|
+
|
225
|
+
# 根据操作类型选择不同的处理逻辑
|
226
|
+
if operation_config.get("is_edit_operation"):
|
227
|
+
# 编辑操作
|
228
|
+
editor = Editor(logger, file_handler)
|
229
|
+
edit_type = operation_config.get("edit_type", "")
|
230
|
+
extra_params = operation_config.get("extra_params")
|
231
|
+
|
232
|
+
# 获取操作描述
|
233
|
+
edit_map = {
|
234
|
+
"decrypt": "解密",
|
235
|
+
"add_watermark": "添加水印",
|
236
|
+
"encrypt": "加密",
|
237
|
+
"compress": "压缩",
|
238
|
+
"split": "拆分",
|
239
|
+
"merge": "合并",
|
240
|
+
"rotate": "旋转"
|
241
|
+
}
|
242
|
+
operation_desc = f"PDF{edit_map.get(edit_type, edit_type)}"
|
243
|
+
|
244
|
+
# 处理文件
|
245
|
+
results = await process_batch_files(
|
246
|
+
file_objects,
|
247
|
+
logger,
|
248
|
+
lambda file_path, password: process_edit_file(
|
249
|
+
file_path, edit_type, editor, extra_params, password
|
250
|
+
),
|
251
|
+
operation_desc
|
252
|
+
)
|
253
|
+
|
254
|
+
# 生成报告
|
255
|
+
report_msg = generate_result_report(
|
256
|
+
results,
|
257
|
+
operation_desc,
|
258
|
+
f"{edit_map.get(edit_type, edit_type)}成功",
|
259
|
+
f"{edit_map.get(edit_type, edit_type)}失败"
|
260
|
+
)
|
261
|
+
else:
|
262
|
+
# 转换操作
|
263
|
+
converter = Converter(logger, file_handler)
|
264
|
+
format = operation_config.get("format", "")
|
265
|
+
extra_params = operation_config.get("extra_params")
|
266
|
+
is_watermark_removal = operation_config.get("is_watermark_removal", False)
|
267
|
+
is_page_numbering = operation_config.get("is_page_numbering", False)
|
268
|
+
|
269
|
+
# 获取操作描述
|
270
|
+
if is_watermark_removal:
|
271
|
+
operation_desc = "去除水印"
|
272
|
+
task_type = "水印去除"
|
273
|
+
success_action = "去除水印成功"
|
274
|
+
failed_action = "水印去除失败"
|
275
|
+
elif is_page_numbering:
|
276
|
+
operation_desc = "添加页码"
|
277
|
+
task_type = "添加页码"
|
278
|
+
success_action = "添加页码成功"
|
279
|
+
failed_action = "添加页码失败"
|
280
|
+
else:
|
281
|
+
operation_desc = f"转换为 {format} 格式"
|
282
|
+
task_type = "转换"
|
283
|
+
success_action = "转换成功"
|
284
|
+
failed_action = "转换失败"
|
285
|
+
|
286
|
+
# 处理文件
|
287
|
+
results = await process_batch_files(
|
288
|
+
file_objects,
|
289
|
+
logger,
|
290
|
+
lambda file_path, password: process_conversion_file(
|
291
|
+
file_path, format, converter, extra_params, password
|
292
|
+
),
|
293
|
+
operation_desc
|
294
|
+
)
|
295
|
+
|
296
|
+
# 生成报告
|
297
|
+
report_msg = generate_result_report(
|
298
|
+
results,
|
299
|
+
task_type,
|
300
|
+
success_action,
|
301
|
+
failed_action
|
302
|
+
)
|
303
|
+
|
304
|
+
# 如果全部失败,记录错误
|
305
|
+
if not any(r.success for r in results):
|
306
|
+
await logger.error(report_msg)
|
307
|
+
|
308
|
+
return types.TextContent(type="text", text=report_msg)
|
309
|
+
|
310
|
+
# 创建Server实例
|
311
|
+
app = Server(
|
312
|
+
name="LightPDF_AI_tools",
|
313
|
+
instructions="轻闪文档处理工具。",
|
314
|
+
)
|
315
|
+
|
316
|
+
# 定义工具
|
317
|
+
@app.list_tools()
|
318
|
+
async def handle_list_tools() -> list[types.Tool]:
|
319
|
+
return [
|
320
|
+
types.Tool(
|
321
|
+
name="convert_document",
|
322
|
+
description="文档格式转换工具。\n\nPDF可转换为:DOCX/XLSX/PPTX/图片/HTML/TXT;\n其他格式可转换为PDF:DOCX/XLSX/PPTX/图片/CAD/CAJ/OFD",
|
323
|
+
inputSchema={
|
324
|
+
"type": "object",
|
325
|
+
"properties": {
|
326
|
+
"files": {
|
327
|
+
"type": "array",
|
328
|
+
"items": {
|
329
|
+
"type": "object",
|
330
|
+
"properties": {
|
331
|
+
"path": {
|
332
|
+
"type": "string",
|
333
|
+
"description": "文件路径或URL"
|
334
|
+
},
|
335
|
+
"password": {
|
336
|
+
"type": "string",
|
337
|
+
"description": "文档密码,如果文档受密码保护,则需要提供此参数"
|
338
|
+
}
|
339
|
+
},
|
340
|
+
"required": ["path"]
|
341
|
+
},
|
342
|
+
"description": "要转换的文件列表,每个文件包含路径和可选的密码"
|
343
|
+
},
|
344
|
+
"format": {
|
345
|
+
"type": "string",
|
346
|
+
"description": "目标格式",
|
347
|
+
"enum": ["pdf", "docx", "xlsx", "pptx", "jpg", "jpeg", "png", "bmp", "gif", "html", "txt"]
|
348
|
+
}
|
349
|
+
},
|
350
|
+
"required": ["files", "format"]
|
351
|
+
}
|
352
|
+
),
|
353
|
+
types.Tool(
|
354
|
+
name="add_page_numbers",
|
355
|
+
description="在PDF文档的每一页上添加页码。",
|
356
|
+
inputSchema={
|
357
|
+
"type": "object",
|
358
|
+
"properties": {
|
359
|
+
"files": {
|
360
|
+
"type": "array",
|
361
|
+
"items": {
|
362
|
+
"type": "object",
|
363
|
+
"properties": {
|
364
|
+
"path": {
|
365
|
+
"type": "string",
|
366
|
+
"description": "PDF文件路径或URL"
|
367
|
+
},
|
368
|
+
"password": {
|
369
|
+
"type": "string",
|
370
|
+
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
371
|
+
}
|
372
|
+
},
|
373
|
+
"required": ["path"]
|
374
|
+
},
|
375
|
+
"description": "要添加页码的PDF文件列表,每个文件包含路径和可选的密码"
|
376
|
+
},
|
377
|
+
"start_num": {
|
378
|
+
"type": "integer",
|
379
|
+
"description": "起始页码",
|
380
|
+
"default": 1,
|
381
|
+
"minimum": 1
|
382
|
+
},
|
383
|
+
"position": {
|
384
|
+
"type": "string",
|
385
|
+
"description": "页码位置:1(左上), 2(上中), 3(右上), 4(左下), 5(下中), 6(右下)",
|
386
|
+
"enum": ["1", "2", "3", "4", "5", "6"],
|
387
|
+
"default": "5"
|
388
|
+
},
|
389
|
+
"margin": {
|
390
|
+
"type": "integer",
|
391
|
+
"description": "页码边距",
|
392
|
+
"enum": [10, 30, 60],
|
393
|
+
"default": 30
|
394
|
+
}
|
395
|
+
},
|
396
|
+
"required": ["files"]
|
397
|
+
}
|
398
|
+
),
|
399
|
+
types.Tool(
|
400
|
+
name="remove_watermark",
|
401
|
+
description="去除PDF文件中的水印,将文件处理后返回无水印的PDF版本。",
|
402
|
+
inputSchema={
|
403
|
+
"type": "object",
|
404
|
+
"properties": {
|
405
|
+
"files": {
|
406
|
+
"type": "array",
|
407
|
+
"items": {
|
408
|
+
"type": "object",
|
409
|
+
"properties": {
|
410
|
+
"path": {
|
411
|
+
"type": "string",
|
412
|
+
"description": "PDF文件路径或URL"
|
413
|
+
},
|
414
|
+
"password": {
|
415
|
+
"type": "string",
|
416
|
+
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
417
|
+
}
|
418
|
+
},
|
419
|
+
"required": ["path"]
|
420
|
+
},
|
421
|
+
"description": "要去除水印的PDF文件列表,每个文件包含路径和可选的密码"
|
422
|
+
}
|
423
|
+
},
|
424
|
+
"required": ["files"]
|
425
|
+
}
|
426
|
+
),
|
427
|
+
types.Tool(
|
428
|
+
name="add_watermark",
|
429
|
+
description="为PDF文件添加文本水印。",
|
430
|
+
inputSchema={
|
431
|
+
"type": "object",
|
432
|
+
"properties": {
|
433
|
+
"files": {
|
434
|
+
"type": "array",
|
435
|
+
"items": {
|
436
|
+
"type": "object",
|
437
|
+
"properties": {
|
438
|
+
"path": {
|
439
|
+
"type": "string",
|
440
|
+
"description": "需要添加水印的PDF文件路径或URL"
|
441
|
+
},
|
442
|
+
"password": {
|
443
|
+
"type": "string",
|
444
|
+
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
445
|
+
}
|
446
|
+
},
|
447
|
+
"required": ["path"]
|
448
|
+
},
|
449
|
+
"description": "需要添加水印的PDF文件列表,每个文件包含路径和可选的密码"
|
450
|
+
},
|
451
|
+
"text": {
|
452
|
+
"type": "string",
|
453
|
+
"description": "水印文本内容"
|
454
|
+
},
|
455
|
+
"position": {
|
456
|
+
"type": "string",
|
457
|
+
"description": "水印位置",
|
458
|
+
"enum": ["topleft", "top", "topright", "left", "center", "right",
|
459
|
+
"bottomleft", "bottom", "bottomright", "diagonal", "reverse-diagonal"],
|
460
|
+
"default": "center"
|
461
|
+
},
|
462
|
+
"opacity": {
|
463
|
+
"type": "number",
|
464
|
+
"description": "透明度,0.0-1.0",
|
465
|
+
"default": 0.5,
|
466
|
+
"minimum": 0.0,
|
467
|
+
"maximum": 1.0
|
468
|
+
},
|
469
|
+
"deg": {
|
470
|
+
"type": "string",
|
471
|
+
"description": "倾斜角度,字符串格式",
|
472
|
+
"default": "0"
|
473
|
+
},
|
474
|
+
"range": {
|
475
|
+
"type": "string",
|
476
|
+
"description": "页面范围,例如 '1,3,5-7' 或 ''(空字符串或不设置)表示所有页面"
|
477
|
+
},
|
478
|
+
"layout": {
|
479
|
+
"type": "string",
|
480
|
+
"description": "布局位置:在内容上(on)或在内容下(under)",
|
481
|
+
"enum": ["on", "under"],
|
482
|
+
"default": "under"
|
483
|
+
},
|
484
|
+
"font_family": {
|
485
|
+
"type": "string",
|
486
|
+
"description": "字体"
|
487
|
+
},
|
488
|
+
"font_size": {
|
489
|
+
"type": "integer",
|
490
|
+
"description": "字体大小"
|
491
|
+
},
|
492
|
+
"font_color": {
|
493
|
+
"type": "string",
|
494
|
+
"description": "字体颜色,如 '#ff0000' 表示红色"
|
495
|
+
}
|
496
|
+
},
|
497
|
+
"required": ["files", "text", "position"]
|
498
|
+
}
|
499
|
+
),
|
500
|
+
types.Tool(
|
501
|
+
name="unlock_pdf",
|
502
|
+
description="移除PDF文件的密码保护,生成无密码版本的PDF文件。",
|
503
|
+
inputSchema={
|
504
|
+
"type": "object",
|
505
|
+
"properties": {
|
506
|
+
"files": {
|
507
|
+
"type": "array",
|
508
|
+
"items": {
|
509
|
+
"type": "object",
|
510
|
+
"properties": {
|
511
|
+
"path": {
|
512
|
+
"type": "string",
|
513
|
+
"description": "需要解密的PDF文件路径或URL"
|
514
|
+
},
|
515
|
+
"password": {
|
516
|
+
"type": "string",
|
517
|
+
"description": "PDF文档的密码,用于解锁文档,如果文档受密码保护,则需要提供此参数"
|
518
|
+
}
|
519
|
+
},
|
520
|
+
"required": ["path", "password"]
|
521
|
+
},
|
522
|
+
"description": "需要解密的PDF文件列表,每个文件包含路径和密码"
|
523
|
+
}
|
524
|
+
},
|
525
|
+
"required": ["files"]
|
526
|
+
}
|
527
|
+
),
|
528
|
+
types.Tool(
|
529
|
+
name="protect_pdf",
|
530
|
+
description="为PDF文件添加密码保护。",
|
531
|
+
inputSchema={
|
532
|
+
"type": "object",
|
533
|
+
"properties": {
|
534
|
+
"files": {
|
535
|
+
"type": "array",
|
536
|
+
"items": {
|
537
|
+
"type": "object",
|
538
|
+
"properties": {
|
539
|
+
"path": {
|
540
|
+
"type": "string",
|
541
|
+
"description": "需要加密的PDF文件路径或URL"
|
542
|
+
},
|
543
|
+
"password": {
|
544
|
+
"type": "string",
|
545
|
+
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
546
|
+
}
|
547
|
+
},
|
548
|
+
"required": ["path"]
|
549
|
+
},
|
550
|
+
"description": "需要加密的PDF文件列表,每个文件包含路径和可选的原密码"
|
551
|
+
},
|
552
|
+
"password": {
|
553
|
+
"type": "string",
|
554
|
+
"description": "要设置的新密码"
|
555
|
+
}
|
556
|
+
},
|
557
|
+
"required": ["files", "password"]
|
558
|
+
}
|
559
|
+
),
|
560
|
+
types.Tool(
|
561
|
+
name="compress_pdf",
|
562
|
+
description="压缩PDF文件大小。",
|
563
|
+
inputSchema={
|
564
|
+
"type": "object",
|
565
|
+
"properties": {
|
566
|
+
"files": {
|
567
|
+
"type": "array",
|
568
|
+
"items": {
|
569
|
+
"type": "object",
|
570
|
+
"properties": {
|
571
|
+
"path": {
|
572
|
+
"type": "string",
|
573
|
+
"description": "需要压缩的PDF文件路径或URL"
|
574
|
+
},
|
575
|
+
"password": {
|
576
|
+
"type": "string",
|
577
|
+
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
578
|
+
}
|
579
|
+
},
|
580
|
+
"required": ["path"]
|
581
|
+
},
|
582
|
+
"description": "需要压缩的PDF文件列表,每个文件包含路径和可选的密码"
|
583
|
+
},
|
584
|
+
"image_quantity": {
|
585
|
+
"type": "integer",
|
586
|
+
"description": "图像质量,1-100,值越低压缩率越高",
|
587
|
+
"default": 60,
|
588
|
+
"minimum": 1,
|
589
|
+
"maximum": 100
|
590
|
+
}
|
591
|
+
},
|
592
|
+
"required": ["files"]
|
593
|
+
}
|
594
|
+
),
|
595
|
+
types.Tool(
|
596
|
+
name="split_pdf",
|
597
|
+
description="拆分PDF文件到多个文件。",
|
598
|
+
inputSchema={
|
599
|
+
"type": "object",
|
600
|
+
"properties": {
|
601
|
+
"files": {
|
602
|
+
"type": "array",
|
603
|
+
"items": {
|
604
|
+
"type": "object",
|
605
|
+
"properties": {
|
606
|
+
"path": {
|
607
|
+
"type": "string",
|
608
|
+
"description": "需要拆分的PDF文件路径或URL"
|
609
|
+
},
|
610
|
+
"password": {
|
611
|
+
"type": "string",
|
612
|
+
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
613
|
+
}
|
614
|
+
},
|
615
|
+
"required": ["path"]
|
616
|
+
},
|
617
|
+
"description": "需要拆分的PDF文件列表,每个文件包含路径和可选的密码"
|
618
|
+
},
|
619
|
+
"split_type": {
|
620
|
+
"type": "string",
|
621
|
+
"description": "拆分类型: 每个页面拆分成一个文件(every)或按pages范围拆分(page)",
|
622
|
+
"enum": ["every", "page"],
|
623
|
+
"default": "page"
|
624
|
+
},
|
625
|
+
"pages": {
|
626
|
+
"type": "string",
|
627
|
+
"description": "指定要拆分的页面范围,例如 '1,3,5-7' 或 ''(空字符串或不设置)表示所有页面。仅当split_type为page时有效"
|
628
|
+
},
|
629
|
+
"merge_all": {
|
630
|
+
"type": "integer",
|
631
|
+
"description": "是否将结果合并为一个PDF文件: 1=是,0=否(将会返回多个文件的zip包)。仅当split_type为page时有效",
|
632
|
+
"enum": [0, 1],
|
633
|
+
"default": 0
|
634
|
+
}
|
635
|
+
},
|
636
|
+
"required": ["files"]
|
637
|
+
}
|
638
|
+
),
|
639
|
+
types.Tool(
|
640
|
+
name="merge_pdfs",
|
641
|
+
description="合并多个PDF文件。",
|
642
|
+
inputSchema={
|
643
|
+
"type": "object",
|
644
|
+
"properties": {
|
645
|
+
"files": {
|
646
|
+
"type": "array",
|
647
|
+
"items": {
|
648
|
+
"type": "object",
|
649
|
+
"properties": {
|
650
|
+
"path": {
|
651
|
+
"type": "string",
|
652
|
+
"description": "需要合并的PDF文件路径或URL"
|
653
|
+
},
|
654
|
+
"password": {
|
655
|
+
"type": "string",
|
656
|
+
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
657
|
+
}
|
658
|
+
},
|
659
|
+
"required": ["path"]
|
660
|
+
},
|
661
|
+
"description": "需要合并的PDF文件列表,每个文件包含路径和可选的密码"
|
662
|
+
}
|
663
|
+
},
|
664
|
+
"required": ["files"]
|
665
|
+
}
|
666
|
+
),
|
667
|
+
types.Tool(
|
668
|
+
name="rotate_pdf",
|
669
|
+
description="旋转PDF文件的页面。",
|
670
|
+
inputSchema={
|
671
|
+
"type": "object",
|
672
|
+
"properties": {
|
673
|
+
"files": {
|
674
|
+
"type": "array",
|
675
|
+
"items": {
|
676
|
+
"type": "object",
|
677
|
+
"properties": {
|
678
|
+
"path": {
|
679
|
+
"type": "string",
|
680
|
+
"description": "需要旋转的PDF文件路径或URL"
|
681
|
+
},
|
682
|
+
"password": {
|
683
|
+
"type": "string",
|
684
|
+
"description": "PDF文档密码,如果文档受密码保护,则需要提供此参数"
|
685
|
+
}
|
686
|
+
},
|
687
|
+
"required": ["path"]
|
688
|
+
},
|
689
|
+
"description": "需要旋转的PDF文件列表,每个文件包含路径和可选的密码"
|
690
|
+
},
|
691
|
+
"angle": {
|
692
|
+
"type": "integer",
|
693
|
+
"description": "旋转角度,可选值为90、180、270",
|
694
|
+
"enum": [90, 180, 270],
|
695
|
+
"default": 90
|
696
|
+
},
|
697
|
+
"pages": {
|
698
|
+
"type": "string",
|
699
|
+
"description": "指定要旋转的页面范围,例如 '1,3,5-7' 或 ''(空字符串或不设置)表示所有页面"
|
700
|
+
}
|
701
|
+
},
|
702
|
+
"required": ["files", "angle"]
|
703
|
+
}
|
704
|
+
)
|
705
|
+
]
|
706
|
+
|
707
|
+
@app.call_tool()
|
708
|
+
async def handle_call_tool(name: str, arguments: dict | None) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
709
|
+
# 创建日志记录器
|
710
|
+
logger = Logger(app.request_context)
|
711
|
+
|
712
|
+
# 定义工具配置和默认参数值
|
713
|
+
TOOL_CONFIG = {
|
714
|
+
"convert_document": {
|
715
|
+
"format_key": "format", # 从arguments获取format
|
716
|
+
"is_watermark_removal": False,
|
717
|
+
"is_page_numbering": False,
|
718
|
+
"is_edit_operation": False,
|
719
|
+
},
|
720
|
+
"remove_watermark": {
|
721
|
+
"format": "doc-repair", # 固定format
|
722
|
+
"is_watermark_removal": True,
|
723
|
+
"is_page_numbering": False,
|
724
|
+
"is_edit_operation": False,
|
725
|
+
},
|
726
|
+
"add_page_numbers": {
|
727
|
+
"format": "number-pdf", # 固定format
|
728
|
+
"is_watermark_removal": False,
|
729
|
+
"is_page_numbering": True,
|
730
|
+
"is_edit_operation": False,
|
731
|
+
"param_keys": ["start_num", "position", "margin"] # 需要从arguments获取的参数
|
732
|
+
},
|
733
|
+
"unlock_pdf": {
|
734
|
+
"edit_type": "decrypt", # 编辑类型
|
735
|
+
"is_edit_operation": True, # 标记为编辑操作
|
736
|
+
},
|
737
|
+
"add_watermark": {
|
738
|
+
"edit_type": "add_watermark", # 编辑类型
|
739
|
+
"is_edit_operation": True, # 标记为编辑操作
|
740
|
+
"param_keys": ["text", "position", "opacity", "deg", "range", "layout",
|
741
|
+
"font_family", "font_size", "font_color"] # 需要从arguments获取的参数
|
742
|
+
},
|
743
|
+
"protect_pdf": {
|
744
|
+
"edit_type": "encrypt", # 编辑类型
|
745
|
+
"is_edit_operation": True, # 标记为编辑操作
|
746
|
+
"param_keys": ["password"] # 需要从arguments获取的参数
|
747
|
+
},
|
748
|
+
"compress_pdf": {
|
749
|
+
"edit_type": "compress", # 编辑类型
|
750
|
+
"is_edit_operation": True, # 标记为编辑操作
|
751
|
+
"param_keys": ["image_quantity"] # 需要从arguments获取的参数
|
752
|
+
},
|
753
|
+
"split_pdf": {
|
754
|
+
"edit_type": "split", # 编辑类型
|
755
|
+
"is_edit_operation": True, # 标记为编辑操作
|
756
|
+
"param_keys": ["pages", "split_type", "merge_all"] # 需要从arguments获取的参数
|
757
|
+
},
|
758
|
+
"merge_pdfs": {
|
759
|
+
"edit_type": "merge", # 编辑类型
|
760
|
+
"is_edit_operation": True, # 标记为编辑操作
|
761
|
+
},
|
762
|
+
"rotate_pdf": {
|
763
|
+
"edit_type": "rotate", # 编辑类型
|
764
|
+
"is_edit_operation": True, # 标记为编辑操作
|
765
|
+
"param_keys": ["angle", "pages"] # 需要从arguments获取的参数
|
766
|
+
}
|
767
|
+
}
|
768
|
+
|
769
|
+
DEFAULTS = {
|
770
|
+
"start_num": 1,
|
771
|
+
"position_page_numbers": "5", # 添加页码的位置默认值
|
772
|
+
"position_watermark": "center", # 水印的位置默认值
|
773
|
+
"margin": 30,
|
774
|
+
"opacity": 0.5,
|
775
|
+
"deg": "0", # 更新为"0"以匹配handle_list_tools中的默认值
|
776
|
+
"range": "",
|
777
|
+
"layout": "under", # 添加layout默认值
|
778
|
+
"image_quantity": 60,
|
779
|
+
"split_type": "page",
|
780
|
+
"merge_all": 1,
|
781
|
+
"angle": 90,
|
782
|
+
"pages": ""
|
783
|
+
}
|
784
|
+
|
785
|
+
if name in TOOL_CONFIG:
|
786
|
+
# 处理文件信息
|
787
|
+
file_objects = arguments.get("files", [])
|
788
|
+
if not file_objects:
|
789
|
+
error_msg = "未提供文件信息"
|
790
|
+
await logger.error(error_msg)
|
791
|
+
return [types.TextContent(type="text", text=error_msg)]
|
792
|
+
|
793
|
+
# 确保file_objects是一个列表
|
794
|
+
if isinstance(file_objects, dict):
|
795
|
+
file_objects = [file_objects]
|
796
|
+
|
797
|
+
config = TOOL_CONFIG[name]
|
798
|
+
operation_config = dict(config) # 复制配置
|
799
|
+
|
800
|
+
# 处理格式
|
801
|
+
if not operation_config.get("format") and "format_key" in config:
|
802
|
+
operation_config["format"] = arguments.get(config["format_key"], "")
|
803
|
+
|
804
|
+
# 处理额外参数
|
805
|
+
if "param_keys" in config:
|
806
|
+
operation_config["extra_params"] = {}
|
807
|
+
|
808
|
+
# 处理特殊情况:position参数在不同工具中有不同的默认值
|
809
|
+
for key in config["param_keys"]:
|
810
|
+
if key == "position":
|
811
|
+
if name == "add_page_numbers":
|
812
|
+
# 添加页码工具使用"5"作为position默认值
|
813
|
+
operation_config["extra_params"][key] = arguments.get(key, DEFAULTS.get("position_page_numbers"))
|
814
|
+
elif name == "add_watermark":
|
815
|
+
# 添加水印工具使用"center"作为position默认值
|
816
|
+
operation_config["extra_params"][key] = arguments.get(key, DEFAULTS.get("position_watermark"))
|
817
|
+
else:
|
818
|
+
# 其他工具使用通用默认值
|
819
|
+
operation_config["extra_params"][key] = arguments.get(key, DEFAULTS.get(key))
|
820
|
+
else:
|
821
|
+
# 其他参数正常处理
|
822
|
+
operation_config["extra_params"][key] = arguments.get(key, DEFAULTS.get(key, ""))
|
823
|
+
|
824
|
+
# 对于protect_pdf工具,需要处理新密码
|
825
|
+
if name == "protect_pdf" and "password" in arguments:
|
826
|
+
operation_config["extra_params"]["password"] = arguments.get("password")
|
827
|
+
|
828
|
+
# 特殊处理merge_pdfs工具
|
829
|
+
if name == "merge_pdfs":
|
830
|
+
# 创建编辑器
|
831
|
+
file_handler = FileHandler(logger)
|
832
|
+
editor = Editor(logger, file_handler)
|
833
|
+
|
834
|
+
try:
|
835
|
+
# 提取文件路径和密码
|
836
|
+
file_paths = [file_obj["path"] for file_obj in file_objects]
|
837
|
+
passwords = [file_obj.get("password") for file_obj in file_objects]
|
838
|
+
|
839
|
+
# 由于merge_pdfs方法只接受一个密码参数,如果文件密码不同,可能需要特殊处理
|
840
|
+
# 此处简化处理,使用第一个非空密码
|
841
|
+
password = next((p for p in passwords if p), None)
|
842
|
+
|
843
|
+
# 直接调用merge_pdfs方法
|
844
|
+
result = await editor.merge_pdfs(file_paths, password)
|
845
|
+
|
846
|
+
# 构建结果报告
|
847
|
+
report_msg = generate_result_report(
|
848
|
+
[result],
|
849
|
+
"PDF合并",
|
850
|
+
"合并成功",
|
851
|
+
"合并失败"
|
852
|
+
)
|
853
|
+
|
854
|
+
# 如果失败,记录错误
|
855
|
+
if not result.success:
|
856
|
+
await logger.error(report_msg)
|
857
|
+
|
858
|
+
return [types.TextContent(type="text", text=report_msg)]
|
859
|
+
except Exception as e:
|
860
|
+
error_msg = f"PDF合并失败: {str(e)}"
|
861
|
+
await logger.error(error_msg)
|
862
|
+
return [types.TextContent(type="text", text=error_msg)]
|
863
|
+
|
864
|
+
# 调用通用处理函数
|
865
|
+
result = await process_tool_call(logger, file_objects, operation_config)
|
866
|
+
return [result]
|
867
|
+
|
868
|
+
error_msg = f"未知工具: {name}"
|
869
|
+
await logger.error(error_msg, ValueError)
|
870
|
+
return [types.TextContent(type="text", text=error_msg)]
|
871
|
+
|
872
|
+
async def main():
|
873
|
+
import mcp.server.stdio as stdio
|
874
|
+
|
875
|
+
async with stdio.stdio_server() as (read_stream, write_stream):
|
876
|
+
await app.run(
|
877
|
+
read_stream,
|
878
|
+
write_stream,
|
879
|
+
app.create_initialization_options(
|
880
|
+
notification_options=NotificationOptions()
|
881
|
+
)
|
882
|
+
)
|
883
|
+
|
884
|
+
def cli_main():
|
885
|
+
asyncio.run(main())
|
886
|
+
|
887
|
+
if __name__ == "__main__":
|
888
|
+
cli_main()
|