mcpcn-office-powerpoint-mcp-server 2.0.8__tar.gz → 2.0.9__tar.gz

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.
Files changed (31) hide show
  1. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/PKG-INFO +1 -1
  2. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/ppt_mcp_server.py +24 -0
  3. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/pyproject.toml +1 -1
  4. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/content_tools.py +118 -5
  5. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/.gitignore +0 -0
  6. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/Dockerfile +0 -0
  7. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/LICENSE +0 -0
  8. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/README.md +0 -0
  9. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/__init__.py +0 -0
  10. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/mcp-config.json +0 -0
  11. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/requirements.txt +0 -0
  12. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/setup_mcp.py +0 -0
  13. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/slide_layout_templates.json +0 -0
  14. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/smithery.yaml +0 -0
  15. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/__init__.py +0 -0
  16. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/chart_tools.py +0 -0
  17. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/connector_tools.py +0 -0
  18. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/hyperlink_tools.py +0 -0
  19. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/master_tools.py +0 -0
  20. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/presentation_tools.py +0 -0
  21. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/professional_tools.py +0 -0
  22. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/structural_tools.py +0 -0
  23. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/template_tools.py +0 -0
  24. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/tools/transition_tools.py +0 -0
  25. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/utils/__init__.py +0 -0
  26. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/utils/content_utils.py +0 -0
  27. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/utils/core_utils.py +0 -0
  28. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/utils/design_utils.py +0 -0
  29. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/utils/presentation_utils.py +0 -0
  30. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/utils/template_utils.py +0 -0
  31. {mcpcn_office_powerpoint_mcp_server-2.0.8 → mcpcn_office_powerpoint_mcp_server-2.0.9}/utils/validation_utils.py +0 -0
@@ -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.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
@@ -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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "mcpcn-office-powerpoint-mcp-server"
7
- version = "2.0.8"
7
+ version = "2.0.9"
8
8
  description = "MCP Server for PowerPoint manipulation using python-pptx - Consolidated Edition"
9
9
  readme = "README.md"
10
10
  license = {file = "LICENSE"}
@@ -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):
@@ -516,6 +524,7 @@ def register_content_tools(app: FastMCP, presentations: Dict, get_current_presen
516
524
  - filter_type: Optional[str] — 过滤器类型(如 "DETAIL"、"SMOOTH" 等,取决于 Pillow 支持)。
517
525
  - output_path: Optional[str] — 增强后图片的输出路径(不传则生成临时文件)。
518
526
  - presentation_id: Optional[str] — 指定演示文稿 ID;不传则使用当前打开的演示文稿。
527
+ 注:在某些部署环境(如你当前环境)中,必须显式传入 presentation_id 才会对目标文档生效。
519
528
 
520
529
  返回
521
530
  - operation="add":
@@ -528,33 +537,40 @@ def register_content_tools(app: FastMCP, presentations: Dict, get_current_presen
528
537
  - 失败时返回 {"error": str}
529
538
 
530
539
  注意事项
531
- - 当前不支持直接传入网络 URL。如需使用网络图片,请先下载为本地文件,或转为 Base64 后将 source_type 设为 "base64"。
540
+ - 现在支持通过 URL 插入图片(仅 operation="add"):source_type="url",仅允许 http/https 协议。
541
+ 会在内部下载到临时文件后插入并自动清理。若返回的 Content-Type 非 image/* 将返回错误。
542
+ enhance 仍然仅支持本地文件路径。
543
+ - 在某些部署环境(如你当前环境)中,需显式提供 presentation_id 参数,否则可能插入到非预期文档或不生效。
532
544
  - operation="enhance" 不接受 Base64,必须提供可访问的本地文件路径。
533
545
  - 插入 Base64 图片时,内部会写入临时文件后再插入,操作完成后临时文件会被清理。
534
546
  - slide_index 必须在当前演示文稿的有效范围内,否则将返回错误。
535
547
 
536
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")
537
553
  - 插入本地图片:
538
554
  manage_image(slide_index=0, operation="add",
539
555
  image_source="D:/images/logo.png", source_type="file",
540
- left=1.0, top=1.0, width=3.0)
556
+ left=1.0, top=1.0, width=3.0, presentation_id="YOUR_PRESENTATION_ID")
541
557
 
542
558
  - 插入 Base64 图片:
543
559
  manage_image(slide_index=1, operation="add",
544
560
  image_source="<BASE64字符串>", source_type="base64",
545
- left=2.0, top=1.5, width=4.0, height=2.5)
561
+ left=2.0, top=1.5, width=4.0, height=2.5, presentation_id="YOUR_PRESENTATION_ID")
546
562
 
547
563
  - 插入并应用专业增强(演示风格):
548
564
  manage_image(slide_index=2, operation="add",
549
565
  image_source="assets/photo.jpg", source_type="file",
550
- enhancement_style="presentation", left=1.0, top=2.0)
566
+ enhancement_style="presentation", left=1.0, top=2.0, presentation_id="YOUR_PRESENTATION_ID")
551
567
 
552
568
  - 增强已有图片文件(自定义参数):
553
569
  manage_image(slide_index=0, operation="enhance",
554
570
  image_source="assets/photo.jpg", source_type="file",
555
571
  brightness=1.2, contrast=1.1, saturation=1.3,
556
572
  sharpness=1.1, blur_radius=0, filter_type=None,
557
- output_path="assets/photo_enhanced.jpg")
573
+ output_path="assets/photo_enhanced.jpg", presentation_id="YOUR_PRESENTATION_ID")
558
574
  """
559
575
  pres_id = presentation_id if presentation_id is not None else get_current_presentation_id()
560
576
 
@@ -596,6 +612,103 @@ def register_content_tools(app: FastMCP, presentations: Dict, get_current_presen
596
612
  return {
597
613
  "error": f"Failed to process base64 image: {str(e)}"
598
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)}"}
599
712
  else:
600
713
  # Handle file path
601
714
  if not os.path.exists(image_source):