mcpcn-office-powerpoint-mcp-server 2.0.7__py3-none-any.whl → 2.0.9__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcpcn-office-powerpoint-mcp-server
3
- Version: 2.0.7
3
+ Version: 2.0.9
4
4
  Summary: MCP Server for PowerPoint manipulation using python-pptx - Consolidated Edition
5
5
  Project-URL: Homepage, https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git
6
6
  Project-URL: Bug Tracker, https://github.com/GongRzhe/Office-PowerPoint-MCP-Server.git/issues
@@ -30,7 +30,7 @@ License-File: LICENSE
30
30
  Classifier: License :: OSI Approved :: MIT License
31
31
  Classifier: Operating System :: OS Independent
32
32
  Classifier: Programming Language :: Python :: 3
33
- Requires-Python: >=3.6
33
+ Requires-Python: >=3.10
34
34
  Requires-Dist: fonttools>=4.0.0
35
35
  Requires-Dist: mcp[cli]>=1.3.0
36
36
  Requires-Dist: pillow>=8.0.0
@@ -1,9 +1,9 @@
1
- ppt_mcp_server.py,sha256=IX9oqlVBRpaBIhWJcIwoUQpdAmeKowtBQatHYSEiDU0,14264
1
+ ppt_mcp_server.py,sha256=YOmBZc_Yh1RWk8QNOveCz-2i5qi3udcmWN0XdvRJopc,15232
2
2
  slide_layout_templates.json,sha256=ryA8zIZv6WBwjhBQaJXRWES3uqE7esqqFmXyiPEdauU,107256
3
3
  tools/__init__.py,sha256=b53px-U7Ny4-FMnAdFLoJMtPxD7lt7DB8ydvcdATCz0,983
4
4
  tools/chart_tools.py,sha256=vvXI53gm9_2mfNOiG-qCfwLdqqr2pe_uDXHN9adVsZk,3124
5
5
  tools/connector_tools.py,sha256=8cUPAfTZ-5vvooLCVQIWfblMzcFe3cPdh_bG5UezykQ,3402
6
- tools/content_tools.py,sha256=uoIg7dJNIJEO_NA5QZfDB7cxGGixN6BTOAaxg8Fnmk4,24996
6
+ tools/content_tools.py,sha256=7rciu_J5N2X4LhwYakBhpPaRxpCLPVM3ObJSskLJniU,35735
7
7
  tools/hyperlink_tools.py,sha256=EweAxbYGvOpNpzDxH5DnrFzGMQmnzNGGehEoCCx9d18,5920
8
8
  tools/master_tools.py,sha256=05a7ZUEDN5a7ySwG97Sj4UJK7Y0pcCxang553Bh7iOM,4986
9
9
  tools/presentation_tools.py,sha256=rnVRYOu4A0Sw97sf8qKBK6-q5dYnK1vO2XNAzN8je4o,7977
@@ -18,8 +18,8 @@ utils/design_utils.py,sha256=vEstbqsBe202PYS4NvqdOLL3Aq5na7G2SBJnY-BYD_g,23761
18
18
  utils/presentation_utils.py,sha256=9Y4F0twPvr0srjq53_Szkwxcn7dh06IWy3snRLaMuM8,6262
19
19
  utils/template_utils.py,sha256=8iSJcJIgoOmi53TtlRnMwRLTDI84EfSW2PWj8EW3Z1k,44574
20
20
  utils/validation_utils.py,sha256=0OiwFE1WZh05ZP-0hLzidyxKSUIJaJ3QrV0_s6FpxZk,11775
21
- mcpcn_office_powerpoint_mcp_server-2.0.7.dist-info/METADATA,sha256=ZCHC_yMRsYUyAJ-5fKfQOEG8kkTcX6DhgbE09dRyKVY,35855
22
- mcpcn_office_powerpoint_mcp_server-2.0.7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- mcpcn_office_powerpoint_mcp_server-2.0.7.dist-info/entry_points.txt,sha256=4iWuiBR6HdlTqSJjCVyqyqVGpSGiD4ssw7LkEzmgvXw,75
24
- mcpcn_office_powerpoint_mcp_server-2.0.7.dist-info/licenses/LICENSE,sha256=A-aSf9c0K2FKh7xtMkSr5Ot76NX8mrdLffsajnmZGKs,1064
25
- mcpcn_office_powerpoint_mcp_server-2.0.7.dist-info/RECORD,,
21
+ mcpcn_office_powerpoint_mcp_server-2.0.9.dist-info/METADATA,sha256=jI7MB6Ie5TvfkTq8Up8zPNNLnU10BUhbLtCnROz0Ftc,35856
22
+ mcpcn_office_powerpoint_mcp_server-2.0.9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ mcpcn_office_powerpoint_mcp_server-2.0.9.dist-info/entry_points.txt,sha256=4iWuiBR6HdlTqSJjCVyqyqVGpSGiD4ssw7LkEzmgvXw,75
24
+ mcpcn_office_powerpoint_mcp_server-2.0.9.dist-info/licenses/LICENSE,sha256=A-aSf9c0K2FKh7xtMkSr5Ot76NX8mrdLffsajnmZGKs,1064
25
+ mcpcn_office_powerpoint_mcp_server-2.0.9.dist-info/RECORD,,
ppt_mcp_server.py CHANGED
@@ -7,6 +7,8 @@ import os
7
7
  import argparse
8
8
  from typing import Dict, Any
9
9
  from mcp.server.fastmcp import FastMCP
10
+ import functools
11
+ import inspect
10
12
 
11
13
  # import utils # Currently unused
12
14
  from tools import (
@@ -27,6 +29,28 @@ app = FastMCP(
27
29
  name="ppt-mcp-server"
28
30
  )
29
31
 
32
+ # Global response wrapper: if a tool returns {"error": ...}, raise to mark outer isError=true
33
+ _original_app_tool = app.tool
34
+ def _tool_with_error_promotion(*t_args, **t_kwargs):
35
+ def _decorator(func):
36
+ @functools.wraps(func)
37
+ def _wrapped(*args, **kwargs):
38
+ result = func(*args, **kwargs)
39
+ # Promote dict-with-error to exception so outer layer sets isError=true
40
+ if isinstance(result, dict) and "error" in result and result["error"]:
41
+ raise RuntimeError(str(result["error"]))
42
+ return result
43
+ # Preserve original callable signature for FastMCP parameter binding
44
+ try:
45
+ _wrapped.__signature__ = inspect.signature(func)
46
+ except Exception:
47
+ pass
48
+ return _original_app_tool(*t_args, **t_kwargs)(_wrapped)
49
+ return _decorator
50
+
51
+ # Monkey patch app.tool before any registrations
52
+ app.tool = _tool_with_error_promotion
53
+
30
54
  # Global state to store presentations in memory
31
55
  presentations = {}
32
56
  current_presentation_id = None
tools/content_tools.py CHANGED
@@ -8,6 +8,14 @@ import utils as ppt_utils
8
8
  import tempfile
9
9
  import base64
10
10
  import os
11
+ import urllib.request
12
+ import urllib.parse
13
+
14
+ # Optional: requests for better HTTP handling
15
+ try:
16
+ import requests # type: ignore
17
+ except Exception:
18
+ requests = None
11
19
 
12
20
 
13
21
  def register_content_tools(app: FastMCP, presentations: Dict, get_current_presentation_id, validate_parameters, is_positive, is_non_negative, is_in_range, is_valid_rgb):
@@ -492,7 +500,78 @@ def register_content_tools(app: FastMCP, presentations: Dict, get_current_presen
492
500
  output_path: Optional[str] = None,
493
501
  presentation_id: Optional[str] = None
494
502
  ) -> Dict:
495
- """Unified image management tool for adding and enhancing images."""
503
+ """
504
+ 统一的图片处理工具(添加/增强)。
505
+
506
+ 功能
507
+ - operation="add":将图片插入到指定幻灯片位置,支持本地文件或 Base64 图片源。
508
+ - operation="enhance":对已有图片文件进行画质增强与风格化处理,输出增强后的图片路径。
509
+
510
+ 参数
511
+ - slide_index: int — 目标幻灯片索引(从 0 开始)。
512
+ - operation: str — "add" 或 "enhance"。
513
+ - image_source: str —
514
+ * 当 source_type="file":本地图片文件路径。
515
+ * 当 source_type="base64":图片的 Base64 字符串。
516
+ - source_type: str — "file" 或 "base64"。
517
+ * add 支持 "file" 与 "base64"。
518
+ * enhance 仅支持 "file"(不接受 base64)。
519
+ - left, top: float — 插入位置(英寸)。
520
+ - width, height: Optional[float] — 插入尺寸(英寸)。可只提供一项以按比例缩放;都不提供则按图片原始尺寸。
521
+ - enhancement_style: Optional[str] — "presentation" 或 "custom"。当 operation="add" 且需要自动增强时可用;"presentation" 走预设的专业增强流程。
522
+ - brightness, contrast, saturation, sharpness: float — 亮度/对比度/饱和度/锐度(默认 1.0,>1 增强,<1 减弱)。
523
+ - blur_radius: float — 模糊半径(默认 0)。
524
+ - filter_type: Optional[str] — 过滤器类型(如 "DETAIL"、"SMOOTH" 等,取决于 Pillow 支持)。
525
+ - output_path: Optional[str] — 增强后图片的输出路径(不传则生成临时文件)。
526
+ - presentation_id: Optional[str] — 指定演示文稿 ID;不传则使用当前打开的演示文稿。
527
+ 注:在某些部署环境(如你当前环境)中,必须显式传入 presentation_id 才会对目标文档生效。
528
+
529
+ 返回
530
+ - operation="add":
531
+ * message: str
532
+ * shape_index: int — 新增形状索引
533
+ * image_path: str(当 source_type="file" 时返回)
534
+ - operation="enhance":
535
+ * message: str
536
+ * enhanced_path: str — 增强后图片文件路径
537
+ - 失败时返回 {"error": str}
538
+
539
+ 注意事项
540
+ - 现在支持通过 URL 插入图片(仅 operation="add"):source_type="url",仅允许 http/https 协议。
541
+ 会在内部下载到临时文件后插入并自动清理。若返回的 Content-Type 非 image/* 将返回错误。
542
+ enhance 仍然仅支持本地文件路径。
543
+ - 在某些部署环境(如你当前环境)中,需显式提供 presentation_id 参数,否则可能插入到非预期文档或不生效。
544
+ - operation="enhance" 不接受 Base64,必须提供可访问的本地文件路径。
545
+ - 插入 Base64 图片时,内部会写入临时文件后再插入,操作完成后临时文件会被清理。
546
+ - slide_index 必须在当前演示文稿的有效范围内,否则将返回错误。
547
+
548
+ 示例
549
+ - 通过 URL 插入图片(仅 add):
550
+ manage_image(slide_index=0, operation="add",
551
+ image_source="https://example.com/logo.png", source_type="url",
552
+ left=1.0, top=1.0, width=3.0, presentation_id="YOUR_PRESENTATION_ID")
553
+ - 插入本地图片:
554
+ manage_image(slide_index=0, operation="add",
555
+ image_source="D:/images/logo.png", source_type="file",
556
+ left=1.0, top=1.0, width=3.0, presentation_id="YOUR_PRESENTATION_ID")
557
+
558
+ - 插入 Base64 图片:
559
+ manage_image(slide_index=1, operation="add",
560
+ image_source="<BASE64字符串>", source_type="base64",
561
+ left=2.0, top=1.5, width=4.0, height=2.5, presentation_id="YOUR_PRESENTATION_ID")
562
+
563
+ - 插入并应用专业增强(演示风格):
564
+ manage_image(slide_index=2, operation="add",
565
+ image_source="assets/photo.jpg", source_type="file",
566
+ enhancement_style="presentation", left=1.0, top=2.0, presentation_id="YOUR_PRESENTATION_ID")
567
+
568
+ - 增强已有图片文件(自定义参数):
569
+ manage_image(slide_index=0, operation="enhance",
570
+ image_source="assets/photo.jpg", source_type="file",
571
+ brightness=1.2, contrast=1.1, saturation=1.3,
572
+ sharpness=1.1, blur_radius=0, filter_type=None,
573
+ output_path="assets/photo_enhanced.jpg", presentation_id="YOUR_PRESENTATION_ID")
574
+ """
496
575
  pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
497
576
 
498
577
  if pres_id is None or pres_id not in presentations:
@@ -533,6 +612,103 @@ def register_content_tools(app: FastMCP, presentations: Dict, get_current_presen
533
612
  return {
534
613
  "error": f"Failed to process base64 image: {str(e)}"
535
614
  }
615
+ elif source_type == "url":
616
+ # Handle image URL (http/https)
617
+ try:
618
+ parsed = urllib.parse.urlparse(image_source)
619
+ if parsed.scheme not in ("http", "https"):
620
+ return {"error": f"Unsupported URL scheme: {parsed.scheme}. Only http/https allowed."}
621
+
622
+ # Download helper using requests if available, else urllib
623
+ content_type = None
624
+ temp_path = None
625
+
626
+ if requests is not None:
627
+ with requests.get(image_source, stream=True) as resp:
628
+ if resp.status_code != 200:
629
+ return {"error": f"Failed to download image. HTTP {resp.status_code}"}
630
+ content_type = resp.headers.get("Content-Type", "")
631
+
632
+ if not content_type.startswith("image/"):
633
+ return {"error": f"URL content is not an image (Content-Type: {content_type or 'unknown'})"}
634
+
635
+ # Determine suffix
636
+ suffix = ".png"
637
+ try:
638
+ main_type = content_type.split(";")[0].strip()
639
+ if "/" in main_type:
640
+ ext = main_type.split("/")[1].lower()
641
+ # Basic normalization
642
+ if ext in ("jpeg", "pjpeg"):
643
+ suffix = ".jpg"
644
+ elif ext in ("png", "gif", "bmp", "webp", "tiff"):
645
+ suffix = f".{ext}"
646
+ except Exception:
647
+ pass
648
+ # Fallback to URL path extension
649
+ if suffix == ".png":
650
+ path_ext = os.path.splitext(parsed.path or "")[1].lower()
651
+ if path_ext in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tif", ".tiff"):
652
+ suffix = path_ext
653
+
654
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
655
+ temp_path = temp_file.name
656
+ total = 0
657
+ for chunk in resp.iter_content(chunk_size=8192):
658
+ if not chunk:
659
+ continue
660
+ temp_file.write(chunk)
661
+ else:
662
+ req = urllib.request.Request(image_source, headers={"User-Agent": "Mozilla/5.0"})
663
+ with urllib.request.urlopen(req) as resp:
664
+ content_type = resp.headers.get("Content-Type", "")
665
+ if not content_type.startswith("image/"):
666
+ return {"error": f"URL content is not an image (Content-Type: {content_type or 'unknown'})"}
667
+
668
+ suffix = ".png"
669
+ try:
670
+ main_type = content_type.split(";")[0].strip()
671
+ if "/" in main_type:
672
+ ext = main_type.split("/")[1].lower()
673
+ if ext in ("jpeg", "pjpeg"):
674
+ suffix = ".jpg"
675
+ elif ext in ("png", "gif", "bmp", "webp", "tiff"):
676
+ suffix = f".{ext}"
677
+ except Exception:
678
+ pass
679
+ if suffix == ".png":
680
+ path_ext = os.path.splitext(parsed.path or "")[1].lower()
681
+ if path_ext in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tif", ".tiff"):
682
+ suffix = path_ext
683
+
684
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
685
+ temp_path = temp_file.name
686
+ total = 0
687
+ while True:
688
+ chunk = resp.read(8192)
689
+ if not chunk:
690
+ break
691
+ temp_file.write(chunk)
692
+
693
+ # Add image from temporary file
694
+ shape = ppt_utils.add_image(slide, temp_path, left, top, width, height)
695
+
696
+ # Clean up temporary file
697
+ if temp_path and os.path.exists(temp_path):
698
+ os.unlink(temp_path)
699
+
700
+ return {
701
+ "message": f"Added image from URL to slide {slide_index}",
702
+ "shape_index": len(slide.shapes) - 1
703
+ }
704
+ except Exception as e:
705
+ # Best-effort cleanup if temp_path was created
706
+ try:
707
+ if temp_path and os.path.exists(temp_path):
708
+ os.unlink(temp_path)
709
+ except Exception:
710
+ pass
711
+ return {"error": f"Failed to process image URL: {str(e)}"}
536
712
  else:
537
713
  # Handle file path
538
714
  if not os.path.exists(image_source):