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.
- {mcpcn_office_powerpoint_mcp_server-2.0.8.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info}/METADATA +1 -1
- {mcpcn_office_powerpoint_mcp_server-2.0.8.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info}/RECORD +7 -7
- ppt_mcp_server.py +24 -0
- tools/content_tools.py +127 -5
- {mcpcn_office_powerpoint_mcp_server-2.0.8.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info}/WHEEL +0 -0
- {mcpcn_office_powerpoint_mcp_server-2.0.8.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info}/entry_points.txt +0 -0
- {mcpcn_office_powerpoint_mcp_server-2.0.8.dist-info → mcpcn_office_powerpoint_mcp_server-2.1.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcpcn-office-powerpoint-mcp-server
|
|
3
|
-
Version: 2.0
|
|
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=
|
|
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=
|
|
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.
|
|
22
|
-
mcpcn_office_powerpoint_mcp_server-2.0.
|
|
23
|
-
mcpcn_office_powerpoint_mcp_server-2.0.
|
|
24
|
-
mcpcn_office_powerpoint_mcp_server-2.0.
|
|
25
|
-
mcpcn_office_powerpoint_mcp_server-2.0.
|
|
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
|
-
-
|
|
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):
|
|
File without changes
|
|
File without changes
|