mcpcn-office-powerpoint-mcp-server 2.0.8__py3-none-any.whl → 2.1.0__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.8
3
+ Version: 2.1.0
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
@@ -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=4tuFTPcDNnR3av1sp0ExgUueHVi6B5CzOjBqVscNV5E,28668
6
+ tools/content_tools.py,sha256=evPaCEUtWlzNG9zPwEhPm7dzVZEuLywNMg8FgOW-6UQ,36683
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.8.dist-info/METADATA,sha256=KIJW0ylNPFVOTYOOL-PmAjo0yJMuiJPZOwWxz0SSvqc,35856
22
- mcpcn_office_powerpoint_mcp_server-2.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
- mcpcn_office_powerpoint_mcp_server-2.0.8.dist-info/entry_points.txt,sha256=4iWuiBR6HdlTqSJjCVyqyqVGpSGiD4ssw7LkEzmgvXw,75
24
- mcpcn_office_powerpoint_mcp_server-2.0.8.dist-info/licenses/LICENSE,sha256=A-aSf9c0K2FKh7xtMkSr5Ot76NX8mrdLffsajnmZGKs,1064
25
- mcpcn_office_powerpoint_mcp_server-2.0.8.dist-info/RECORD,,
21
+ mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info/METADATA,sha256=j8wH5kHpR1e_hGCcGrut-1JI5fcxc5rOll9zOPLdKUg,35856
22
+ mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info/entry_points.txt,sha256=4iWuiBR6HdlTqSJjCVyqyqVGpSGiD4ssw7LkEzmgvXw,75
24
+ mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info/licenses/LICENSE,sha256=A-aSf9c0K2FKh7xtMkSr5Ot76NX8mrdLffsajnmZGKs,1064
25
+ mcpcn_office_powerpoint_mcp_server-2.1.0.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):
@@ -325,6 +333,9 @@ def register_content_tools(app: FastMCP, presentations: Dict, get_current_presen
325
333
 
326
334
  try:
327
335
  if operation == "add":
336
+ # Auto-detect URL even if source_type is not explicitly "url"
337
+ if isinstance(image_source, str) and (image_source.startswith("http://") or image_source.startswith("https://")):
338
+ source_type = "url"
328
339
  # Add new textbox
329
340
  shape = ppt_utils.add_textbox(
330
341
  slide, left, top, width, height, text,
@@ -516,6 +527,7 @@ def register_content_tools(app: FastMCP, presentations: Dict, get_current_presen
516
527
  - filter_type: Optional[str] — 过滤器类型(如 "DETAIL"、"SMOOTH" 等,取决于 Pillow 支持)。
517
528
  - output_path: Optional[str] — 增强后图片的输出路径(不传则生成临时文件)。
518
529
  - presentation_id: Optional[str] — 指定演示文稿 ID;不传则使用当前打开的演示文稿。
530
+ 注:在某些部署环境(如你当前环境)中,必须显式传入 presentation_id 才会对目标文档生效。
519
531
 
520
532
  返回
521
533
  - operation="add":
@@ -528,33 +540,40 @@ def register_content_tools(app: FastMCP, presentations: Dict, get_current_presen
528
540
  - 失败时返回 {"error": str}
529
541
 
530
542
  注意事项
531
- - 当前不支持直接传入网络 URL。如需使用网络图片,请先下载为本地文件,或转为 Base64 后将 source_type 设为 "base64"。
543
+ - 现在支持通过 URL 插入图片(仅 operation="add"):source_type="url",仅允许 http/https 协议。
544
+ 会在内部下载到临时文件后插入并自动清理。若返回的 Content-Type 非 image/* 将返回错误。
545
+ enhance 仍然仅支持本地文件路径。
546
+ - 在某些部署环境(如你当前环境)中,需显式提供 presentation_id 参数,否则可能插入到非预期文档或不生效。
532
547
  - operation="enhance" 不接受 Base64,必须提供可访问的本地文件路径。
533
548
  - 插入 Base64 图片时,内部会写入临时文件后再插入,操作完成后临时文件会被清理。
534
549
  - slide_index 必须在当前演示文稿的有效范围内,否则将返回错误。
535
550
 
536
551
  示例
552
+ - 通过 URL 插入图片(仅 add):
553
+ manage_image(slide_index=0, operation="add",
554
+ image_source="https://example.com/logo.png", source_type="url",
555
+ left=1.0, top=1.0, width=3.0, presentation_id="YOUR_PRESENTATION_ID")
537
556
  - 插入本地图片:
538
557
  manage_image(slide_index=0, operation="add",
539
558
  image_source="D:/images/logo.png", source_type="file",
540
- left=1.0, top=1.0, width=3.0)
559
+ left=1.0, top=1.0, width=3.0, presentation_id="YOUR_PRESENTATION_ID")
541
560
 
542
561
  - 插入 Base64 图片:
543
562
  manage_image(slide_index=1, operation="add",
544
563
  image_source="<BASE64字符串>", source_type="base64",
545
- left=2.0, top=1.5, width=4.0, height=2.5)
564
+ left=2.0, top=1.5, width=4.0, height=2.5, presentation_id="YOUR_PRESENTATION_ID")
546
565
 
547
566
  - 插入并应用专业增强(演示风格):
548
567
  manage_image(slide_index=2, operation="add",
549
568
  image_source="assets/photo.jpg", source_type="file",
550
- enhancement_style="presentation", left=1.0, top=2.0)
569
+ enhancement_style="presentation", left=1.0, top=2.0, presentation_id="YOUR_PRESENTATION_ID")
551
570
 
552
571
  - 增强已有图片文件(自定义参数):
553
572
  manage_image(slide_index=0, operation="enhance",
554
573
  image_source="assets/photo.jpg", source_type="file",
555
574
  brightness=1.2, contrast=1.1, saturation=1.3,
556
575
  sharpness=1.1, blur_radius=0, filter_type=None,
557
- output_path="assets/photo_enhanced.jpg")
576
+ output_path="assets/photo_enhanced.jpg", presentation_id="YOUR_PRESENTATION_ID")
558
577
  """
559
578
  pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
560
579
 
@@ -596,6 +615,109 @@ def register_content_tools(app: FastMCP, presentations: Dict, get_current_presen
596
615
  return {
597
616
  "error": f"Failed to process base64 image: {str(e)}"
598
617
  }
618
+ elif source_type == "url":
619
+ # Handle image URL (http/https)
620
+ try:
621
+ # Normalize and percent-encode URL path/query to support spaces and non-ASCII characters
622
+ parsed = urllib.parse.urlsplit(image_source)
623
+ if parsed.scheme not in ("http", "https"):
624
+ return {"error": f"Unsupported URL scheme: {parsed.scheme}. Only http/https allowed."}
625
+ encoded_path = urllib.parse.quote(parsed.path or "", safe="/%")
626
+ # Re-encode query preserving keys and multiple values
627
+ qsl = urllib.parse.parse_qsl(parsed.query or "", keep_blank_values=True)
628
+ encoded_query = urllib.parse.urlencode(qsl, doseq=True)
629
+ encoded_url = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, encoded_path, encoded_query, parsed.fragment))
630
+
631
+ # Download helper using requests if available, else urllib
632
+ content_type = None
633
+ temp_path = None
634
+ image_exts = (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tif", ".tiff")
635
+
636
+ if requests is not None:
637
+ with requests.get(encoded_url, stream=True) as resp:
638
+ if resp.status_code != 200:
639
+ return {"error": f"Failed to download image. HTTP {resp.status_code}"}
640
+ content_type = resp.headers.get("Content-Type", "") or ""
641
+
642
+ # Determine suffix and allow fallback by URL extension if Content-Type missing or not image/*
643
+ suffix = ".png"
644
+ is_image = content_type.startswith("image/")
645
+ try:
646
+ main_type = (content_type.split(";")[0].strip() if content_type else "")
647
+ if "/" in main_type:
648
+ ext = main_type.split("/")[1].lower()
649
+ if ext in ("jpeg", "pjpeg"):
650
+ suffix = ".jpg"
651
+ elif ext in ("png", "gif", "bmp", "webp", "tiff"):
652
+ suffix = f".{ext}"
653
+ except Exception:
654
+ pass
655
+ if suffix == ".png":
656
+ path_ext = os.path.splitext(parsed.path or "")[1].lower()
657
+ if path_ext in image_exts:
658
+ suffix = path_ext
659
+
660
+ if not is_image and suffix not in image_exts:
661
+ return {"error": f"URL content is not an image (Content-Type: {content_type or 'unknown'})"}
662
+
663
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
664
+ temp_path = temp_file.name
665
+ for chunk in resp.iter_content(chunk_size=8192):
666
+ if not chunk:
667
+ continue
668
+ temp_file.write(chunk)
669
+ else:
670
+ req = urllib.request.Request(encoded_url, headers={"User-Agent": "Mozilla/5.0"})
671
+ with urllib.request.urlopen(req) as resp:
672
+ content_type = resp.headers.get("Content-Type", "") or ""
673
+
674
+ suffix = ".png"
675
+ is_image = content_type.startswith("image/")
676
+ try:
677
+ main_type = (content_type.split(";")[0].strip() if content_type else "")
678
+ if "/" in main_type:
679
+ ext = main_type.split("/")[1].lower()
680
+ if ext in ("jpeg", "pjpeg"):
681
+ suffix = ".jpg"
682
+ elif ext in ("png", "gif", "bmp", "webp", "tiff"):
683
+ suffix = f".{ext}"
684
+ except Exception:
685
+ pass
686
+ if suffix == ".png":
687
+ path_ext = os.path.splitext(parsed.path or "")[1].lower()
688
+ if path_ext in image_exts:
689
+ suffix = path_ext
690
+
691
+ if not is_image and suffix not in image_exts:
692
+ return {"error": f"URL content is not an image (Content-Type: {content_type or 'unknown'})"}
693
+
694
+ with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file:
695
+ temp_path = temp_file.name
696
+ while True:
697
+ chunk = resp.read(8192)
698
+ if not chunk:
699
+ break
700
+ temp_file.write(chunk)
701
+
702
+ # Add image from temporary file
703
+ shape = ppt_utils.add_image(slide, temp_path, left, top, width, height)
704
+
705
+ # Clean up temporary file
706
+ if temp_path and os.path.exists(temp_path):
707
+ os.unlink(temp_path)
708
+
709
+ return {
710
+ "message": f"Added image from URL to slide {slide_index}",
711
+ "shape_index": len(slide.shapes) - 1
712
+ }
713
+ except Exception as e:
714
+ # Best-effort cleanup if temp_path was created
715
+ try:
716
+ if temp_path and os.path.exists(temp_path):
717
+ os.unlink(temp_path)
718
+ except Exception:
719
+ pass
720
+ return {"error": f"Failed to process image URL: {str(e)}"}
599
721
  else:
600
722
  # Handle file path
601
723
  if not os.path.exists(image_source):