MCP Unified Server
by getfounded
- tools
#!/usr/bin/env python3
import os
import json
import re
import io
import base64
from enum import Enum
from typing import List, Dict, Any, Optional, Union, Tuple
from dataclasses import dataclass
import tempfile
from datetime import datetime
# PowerPoint manipulation
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
from pptx.enum.text import PP_ALIGN
from pptx.enum.shapes import MSO_SHAPE
from pptx.dml.color import RGBColor
from pptx.util import Inches, Pt
from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE
# NLP
import nltk
from nltk.tokenize import word_tokenize, sent_tokenize
from nltk.corpus import stopwords
# Image processing
from PIL import Image
# For MCP integration
from mcp.server.fastmcp import FastMCP, Context
from mcp.server.fastmcp import Image as MCPImage
from mcp.types import Tool, TextContent, ImageContent
# Allow for external mcp instance to be provided
# This will be set when imported by mcp_unified_server.py
_external_mcp = None
# Create local mcp instance (only used when running standalone)
_local_mcp = FastMCP(
"PowerPoint Tools",
dependencies=["python-pptx", "nltk", "pillow"]
)
# Helper function to get the correct mcp instance
def get_mcp():
return _external_mcp if _external_mcp else _local_mcp
# For backward compatibility, expose as mcp
mcp = get_mcp()
# Define PowerPoint Tool operations
class PowerPointTools(str, Enum):
CREATE_PRESENTATION = "ppt_create_presentation"
OPEN_PRESENTATION = "ppt_open_presentation"
SAVE_PRESENTATION = "ppt_save_presentation"
ADD_SLIDE = "ppt_add_slide"
ADD_TEXT = "ppt_add_text"
ADD_IMAGE = "ppt_add_image"
ADD_CHART = "ppt_add_chart"
ADD_TABLE = "ppt_add_table"
ANALYZE_PRESENTATION = "ppt_analyze_presentation"
GENERATE_PRESENTATION = "ppt_generate_presentation"
ENHANCE_PRESENTATION = "ppt_enhance_presentation"
# PowerPoint Session Manager
class PowerPointManager:
"""Manages PowerPoint presentation sessions"""
def __init__(self):
self.active_presentations = {}
self.temp_dir = tempfile.mkdtemp()
self.screenshots = {} # Add this line to store screenshots
def store_screenshot(self, name, image_data):
"""Store a screenshot"""
self.screenshots[name] = image_data
return f"Screenshot '{name}' stored"
def create_presentation(self, session_id: str, template_path: Optional[str] = None) -> str:
"""Create a new PowerPoint presentation"""
try:
if template_path and os.path.exists(template_path):
prs = Presentation(template_path)
else:
prs = Presentation()
self.active_presentations[session_id] = {
"presentation": prs,
"file_path": None,
"created_at": datetime.now(),
"modified_at": datetime.now()
}
return f"Created new presentation with session ID: {session_id}"
except Exception as e:
return f"Error creating presentation: {str(e)}"
def open_presentation(self, session_id: str, file_path: str) -> str:
"""Open an existing PowerPoint presentation"""
try:
if not os.path.exists(file_path):
return f"File not found: {file_path}"
prs = Presentation(file_path)
self.active_presentations[session_id] = {
"presentation": prs,
"file_path": file_path,
"created_at": datetime.now(),
"modified_at": datetime.now()
}
return f"Opened presentation from {file_path} with session ID: {session_id}"
except Exception as e:
return f"Error opening presentation: {str(e)}"
def save_presentation(self, session_id: str, file_path: Optional[str] = None) -> str:
"""Save the active PowerPoint presentation"""
try:
if session_id not in self.active_presentations:
return f"Session not found: {session_id}"
prs = self.active_presentations[session_id]["presentation"]
save_path = file_path or self.active_presentations[session_id]["file_path"]
if not save_path:
save_path = os.path.join(self.temp_dir, f"{session_id}.pptx")
prs.save(save_path)
self.active_presentations[session_id]["file_path"] = save_path
self.active_presentations[session_id]["modified_at"] = datetime.now()
return f"Saved presentation to {save_path}"
except Exception as e:
return f"Error saving presentation: {str(e)}"
def add_slide(self, session_id: str, layout_index: int = 1, title: Optional[str] = None,
content: Optional[str] = None) -> str:
"""Add a new slide to the presentation"""
try:
if session_id not in self.active_presentations:
return f"Session not found: {session_id}"
prs = self.active_presentations[session_id]["presentation"]
# Get layout from master slides (default to title and content)
try:
layout = prs.slide_layouts[layout_index]
except IndexError:
layout = prs.slide_layouts[1] # Default to title and content
# Add slide
slide = prs.slides.add_slide(layout)
# Add title if provided
if title and hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title'):
slide.shapes.title.text = title
# Add content if provided
if content and len(slide.placeholders) > 1:
for shape in slide.placeholders:
if shape.placeholder_format.type == 1: # Title
continue
if shape.placeholder_format.type in (2, 3, 7): # Content, Text, Body
shape.text = content
break
self.active_presentations[session_id]["modified_at"] = datetime.now()
return f"Added slide with layout index {layout_index}"
except Exception as e:
return f"Error adding slide: {str(e)}"
def add_text(self, session_id: str, slide_index: int, text: str,
left: float = 1.0, top: float = 1.0, width: float = 8.0, height: float = 1.0,
font_size: int = 18, font_name: str = 'Calibri', bold: bool = False,
italic: bool = False, color: str = '000000') -> str:
"""Add text box to a slide"""
try:
if session_id not in self.active_presentations:
return f"Session not found: {session_id}"
prs = self.active_presentations[session_id]["presentation"]
try:
slide = prs.slides[slide_index]
except IndexError:
return f"Slide index {slide_index} out of range"
# Convert measurements to PowerPoint's units (inches)
left_inches = Inches(left)
top_inches = Inches(top)
width_inches = Inches(width)
height_inches = Inches(height)
# Add text box
textbox = slide.shapes.add_textbox(left_inches, top_inches, width_inches, height_inches)
textframe = textbox.text_frame
textframe.text = text
# Format text
paragraph = textframe.paragraphs[0]
run = paragraph.runs[0]
run.font.size = Pt(font_size)
run.font.name = font_name
run.font.bold = bold
run.font.italic = italic
# Set color (expecting hex color code without #)
if color and len(color) == 6:
try:
r = int(color[0:2], 16)
g = int(color[2:4], 16)
b = int(color[4:6], 16)
run.font.color.rgb = RGBColor(r, g, b)
except ValueError:
pass # Invalid color code, ignore
self.active_presentations[session_id]["modified_at"] = datetime.now()
return f"Added text box to slide {slide_index}"
except Exception as e:
return f"Error adding text: {str(e)}"
def add_image(self, session_id: str, slide_index: int, image_path: str,
left: float = 1.0, top: float = 1.0, width: Optional[float] = None,
height: Optional[float] = None) -> str:
"""Add image to a slide"""
try:
if session_id not in self.active_presentations:
return f"Session not found: {session_id}"
prs = self.active_presentations[session_id]["presentation"]
try:
slide = prs.slides[slide_index]
except IndexError:
return f"Slide index {slide_index} out of range"
# Check if image file exists
if not os.path.exists(image_path):
return f"Image file not found: {image_path}"
# Convert measurements to PowerPoint's units (inches)
left_inches = Inches(left)
top_inches = Inches(top)
# Determine image size
if width is not None and height is not None:
width_inches = Inches(width)
height_inches = Inches(height)
slide.shapes.add_picture(image_path, left_inches, top_inches, width_inches, height_inches)
else:
slide.shapes.add_picture(image_path, left_inches, top_inches)
self.active_presentations[session_id]["modified_at"] = datetime.now()
return f"Added image to slide {slide_index}"
except Exception as e:
return f"Error adding image: {str(e)}"
def add_chart(self, session_id: str, slide_index: int, chart_type: str,
categories: List[str], series_names: List[str], series_values: List[List[float]],
left: float = 1.0, top: float = 1.0, width: float = 8.0, height: float = 5.0,
chart_title: Optional[str] = None) -> str:
"""Add chart to a slide"""
try:
if session_id not in self.active_presentations:
return f"Session not found: {session_id}"
prs = self.active_presentations[session_id]["presentation"]
try:
slide = prs.slides[slide_index]
except IndexError:
return f"Slide index {slide_index} out of range"
# Convert measurements to PowerPoint's units (inches)
left_inches = Inches(left)
top_inches = Inches(top)
width_inches = Inches(width)
height_inches = Inches(height)
# Map chart type string to PowerPoint chart type
chart_type_map = {
'column': XL_CHART_TYPE.COLUMN_CLUSTERED,
'bar': XL_CHART_TYPE.BAR_CLUSTERED,
'line': XL_CHART_TYPE.LINE,
'pie': XL_CHART_TYPE.PIE,
'area': XL_CHART_TYPE.AREA,
'scatter': XL_CHART_TYPE.XY_SCATTER,
'radar': XL_CHART_TYPE.RADAR,
'stock': XL_CHART_TYPE.STOCK_HLOC,
'surface': XL_CHART_TYPE.SURFACE,
'doughnut': XL_CHART_TYPE.DOUGHNUT,
'bubble': XL_CHART_TYPE.BUBBLE
}
xl_chart_type = chart_type_map.get(chart_type.lower(), XL_CHART_TYPE.COLUMN_CLUSTERED)
# Create chart data
chart_data = CategoryChartData()
chart_data.categories = categories
# Add series
for i, series_name in enumerate(series_names):
if i < len(series_values):
chart_data.add_series(series_name, series_values[i])
# Add chart to slide
chart = slide.shapes.add_chart(xl_chart_type, left_inches, top_inches,
width_inches, height_inches, chart_data).chart
# Set chart title if provided
if chart_title:
chart.has_title = True
chart.chart_title.text_frame.text = chart_title
self.active_presentations[session_id]["modified_at"] = datetime.now()
return f"Added {chart_type} chart to slide {slide_index}"
except Exception as e:
return f"Error adding chart: {str(e)}"
def add_table(self, session_id: str, slide_index: int, rows: int, cols: int,
data: List[List[str]], left: float = 1.0, top: float = 1.0,
width: float = 8.0, height: float = 5.0) -> str:
"""Add table to a slide"""
try:
if session_id not in self.active_presentations:
return f"Session not found: {session_id}"
prs = self.active_presentations[session_id]["presentation"]
try:
slide = prs.slides[slide_index]
except IndexError:
return f"Slide index {slide_index} out of range"
# Convert measurements to PowerPoint's units (inches)
left_inches = Inches(left)
top_inches = Inches(top)
width_inches = Inches(width)
height_inches = Inches(height)
# Add table
table = slide.shapes.add_table(rows, cols, left_inches, top_inches,
width_inches, height_inches).table
# Populate table data
for r in range(min(rows, len(data))):
for c in range(min(cols, len(data[r]) if r < len(data) else 0)):
cell = table.cell(r, c)
cell.text = str(data[r][c])
self.active_presentations[session_id]["modified_at"] = datetime.now()
return f"Added table with {rows} rows and {cols} columns to slide {slide_index}"
except Exception as e:
return f"Error adding table: {str(e)}"
def analyze_presentation(self, session_id: str) -> Dict[str, Any]:
"""Analyze the content and structure of a presentation"""
try:
if session_id not in self.active_presentations:
return {"error": f"Session not found: {session_id}"}
prs = self.active_presentations[session_id]["presentation"]
analysis = {
"total_slides": len(prs.slides),
"slides": [],
"word_count": 0,
"total_images": 0,
"total_charts": 0,
"total_tables": 0,
"average_words_per_slide": 0,
"slide_titles": [],
"presentation_structure": []
}
for i, slide in enumerate(prs.slides):
slide_analysis = self._analyze_slide(slide)
slide_analysis["slide_number"] = i
analysis["slides"].append(slide_analysis)
# Update presentation-wide statistics
analysis["word_count"] += slide_analysis["word_count"]
analysis["total_images"] += slide_analysis["image_count"]
analysis["total_charts"] += slide_analysis["chart_count"]
analysis["total_tables"] += slide_analysis["table_count"]
# Get slide title
if slide_analysis["title"]:
analysis["slide_titles"].append(slide_analysis["title"])
analysis["presentation_structure"].append({
"slide_number": i,
"title": slide_analysis["title"]
})
else:
analysis["presentation_structure"].append({
"slide_number": i,
"title": f"Slide {i+1} (No Title)"
})
# Calculate averages
if analysis["total_slides"] > 0:
analysis["average_words_per_slide"] = analysis["word_count"] / analysis["total_slides"]
return analysis
except Exception as e:
return {"error": f"Error analyzing presentation: {str(e)}"}
def _analyze_slide(self, slide) -> Dict[str, Any]:
"""Analyze a single slide"""
slide_analysis = {
"title": "",
"word_count": 0,
"text_content": "",
"image_count": 0,
"chart_count": 0,
"table_count": 0,
"elements": []
}
# Get slide title
if hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title') and slide.shapes.title is not None:
slide_analysis["title"] = slide.shapes.title.text
# Analyze shapes
for shape in slide.shapes:
# Text analysis
if shape.has_text_frame:
text = ""
for paragraph in shape.text_frame.paragraphs:
text += paragraph.text + "\n"
if text.strip():
slide_analysis["text_content"] += text
words = text.split()
slide_analysis["word_count"] += len(words)
slide_analysis["elements"].append({
"type": "text",
"content": text.strip(),
"word_count": len(words)
})
# Image analysis
if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
slide_analysis["image_count"] += 1
slide_analysis["elements"].append({
"type": "image"
})
# Chart analysis
if shape.has_chart:
slide_analysis["chart_count"] += 1
slide_analysis["elements"].append({
"type": "chart",
"chart_type": str(shape.chart.chart_type)
})
# Table analysis
if shape.has_table:
slide_analysis["table_count"] += 1
rows = shape.table.rows
cols = shape.table.columns
table_data = []
for r in range(len(rows)):
row_data = []
for c in range(len(cols)):
cell_text = shape.table.cell(r, c).text
row_data.append(cell_text)
table_data.append(row_data)
slide_analysis["elements"].append({
"type": "table",
"rows": len(rows),
"columns": len(cols),
"data": table_data
})
return slide_analysis
def generate_enhancement_suggestions(self, session_id: str) -> Dict[str, Any]:
"""Generate suggestions to enhance the presentation"""
try:
analysis = self.analyze_presentation(session_id)
if "error" in analysis:
return {"error": analysis["error"]}
suggestions = {
"overall_suggestions": [],
"slide_suggestions": []
}
# Overall suggestions
self._add_overall_suggestions(analysis, suggestions)
# Per-slide suggestions
for slide_analysis in analysis["slides"]:
slide_suggestions = self._generate_slide_suggestions(slide_analysis)
suggestions["slide_suggestions"].append({
"slide_number": slide_analysis["slide_number"],
"title": slide_analysis["title"] or f"Slide {slide_analysis['slide_number']+1}",
"suggestions": slide_suggestions
})
return suggestions
except Exception as e:
return {"error": f"Error generating enhancement suggestions: {str(e)}"}
def _add_overall_suggestions(self, analysis, suggestions):
"""Add overall presentation suggestions"""
# Check for consistent structure
if analysis["total_slides"] > 1:
suggestions["overall_suggestions"].append({
"type": "structure",
"suggestion": "Consider adding an agenda or table of contents slide at the beginning"
})
# Check word count
if analysis["average_words_per_slide"] > 100:
suggestions["overall_suggestions"].append({
"type": "content_density",
"suggestion": "Presentation slides have too much text (average over 100 words per slide). Consider breaking content into more slides or reducing text."
})
# Check slide count
if analysis["total_slides"] > 15:
suggestions["overall_suggestions"].append({
"type": "length",
"suggestion": "Presentation is quite long. Consider condensing or breaking into multiple presentations."
})
# Check visual elements
if analysis["total_images"] + analysis["total_charts"] < analysis["total_slides"] / 2:
suggestions["overall_suggestions"].append({
"type": "visuals",
"suggestion": "Add more visual elements (images, charts) to increase engagement."
})
def _generate_slide_suggestions(self, slide_analysis):
"""Generate suggestions for a single slide"""
suggestions = []
# Check for title
if not slide_analysis["title"]:
suggestions.append({
"type": "structure",
"suggestion": "Add a clear title to this slide"
})
# Check word count on slide
if slide_analysis["word_count"] > 100:
suggestions.append({
"type": "content_density",
"suggestion": "Slide has too much text. Consider breaking into multiple slides or reducing text."
})
elif slide_analysis["word_count"] < 10 and slide_analysis["image_count"] == 0 and slide_analysis["chart_count"] == 0:
suggestions.append({
"type": "content",
"suggestion": "Slide has very little content. Consider adding more information or visuals."
})
# Check balance of elements
if slide_analysis["word_count"] > 50 and slide_analysis["image_count"] + slide_analysis["chart_count"] == 0:
suggestions.append({
"type": "balance",
"suggestion": "Text-heavy slide. Consider adding relevant images or charts to illustrate points."
})
return suggestions
def generate_presentation_from_content(self, session_id: str, title: str, content: Dict[str, Any]) -> str:
"""Generate a presentation from structured content"""
try:
# Create new presentation
result = self.create_presentation(session_id)
if "Error" in result:
return result
prs = self.active_presentations[session_id]["presentation"]
# Add title slide
title_slide_layout = prs.slide_layouts[0]
slide = prs.slides.add_slide(title_slide_layout)
slide.shapes.title.text = title
if "subtitle" in content:
slide.placeholders[1].text = content["subtitle"]
# Add content slides
if "slides" in content and isinstance(content["slides"], list):
for slide_content in content["slides"]:
self._add_content_slide(prs, slide_content)
return f"Generated presentation with title: {title}"
except Exception as e:
return f"Error generating presentation: {str(e)}"
def _add_content_slide(self, prs, slide_content):
"""Add a content slide based on structured data"""
# Determine slide layout
layout_index = 1 # Default to Title and Content
if "layout" in slide_content:
layout_map = {
"title": 0,
"title_content": 1,
"section": 2,
"two_content": 3,
"comparison": 4,
"title_only": 5,
"blank": 6,
"content_caption": 7,
"picture_caption": 8
}
layout_index = layout_map.get(slide_content["layout"].lower(), 1)
# Try to use specified layout, fallback to simpler layouts if not available
try:
layout = prs.slide_layouts[layout_index]
except IndexError:
# Fallback to common layouts
for index in [1, 5, 6]:
try:
layout = prs.slide_layouts[index]
break
except IndexError:
continue
else:
# If all fallbacks fail, use the first available layout
layout = prs.slide_layouts[0]
# Add slide
slide = prs.slides.add_slide(layout)
# Add title if provided
if "title" in slide_content and hasattr(slide, 'shapes') and hasattr(slide.shapes, 'title'):
slide.shapes.title.text = slide_content["title"]
# Add text content
if "content" in slide_content:
# Try to find content placeholder
for shape in slide.placeholders:
if shape.placeholder_format.type in (2, 3, 7): # Content, Text, Body
shape.text = slide_content["content"]
break
else:
# If no suitable placeholder found, add textbox
left = Inches(1)
top = Inches(2)
width = Inches(8)
height = Inches(4)
textbox = slide.shapes.add_textbox(left, top, width, height)
textbox.text_frame.text = slide_content["content"]
# Add image if specified
if "image_path" in slide_content and os.path.exists(slide_content["image_path"]):
# Default position
left = Inches(3)
top = Inches(3)
# Try to find image placeholder
for shape in slide.placeholders:
if shape.placeholder_format.type == 18: # Picture
left, top, width, height = shape.left, shape.top, shape.width, shape.height
slide.shapes.add_picture(slide_content["image_path"], left, top, width, height)
break
else:
# If no picture placeholder, add image to slide
slide.shapes.add_picture(slide_content["image_path"], left, top)
# Add chart if specified
if "chart" in slide_content:
chart_info = slide_content["chart"]
if ("type" in chart_info and "categories" in chart_info and
"series_names" in chart_info and "series_values" in chart_info):
# Default position
left = Inches(1.5)
top = Inches(2)
width = Inches(7)
height = Inches(5)
# Try to find chart placeholder
for shape in slide.placeholders:
if shape.placeholder_format.type in (2, 3): # Content, Object
left, top, width, height = shape.left, shape.top, shape.width, shape.height
break
# Map chart type
chart_type_map = {
'column': XL_CHART_TYPE.COLUMN_CLUSTERED,
'bar': XL_CHART_TYPE.BAR_CLUSTERED,
'line': XL_CHART_TYPE.LINE,
'pie': XL_CHART_TYPE.PIE,
'area': XL_CHART_TYPE.AREA
}
xl_chart_type = chart_type_map.get(
chart_info["type"].lower(),
XL_CHART_TYPE.COLUMN_CLUSTERED
)
# Create chart data
chart_data = CategoryChartData()
chart_data.categories = chart_info["categories"]
# Add series
for i, series_name in enumerate(chart_info["series_names"]):
if i < len(chart_info["series_values"]):
chart_data.add_series(series_name, chart_info["series_values"][i])
# Add chart to slide
chart = slide.shapes.add_chart(
xl_chart_type, left, top, width, height, chart_data
).chart
# Set chart title if provided
if "title" in chart_info:
chart.has_title = True
chart.chart_title.text_frame.text = chart_info["title"]
# Add table if specified
if "table" in slide_content:
table_info = slide_content["table"]
if "rows" in table_info and "cols" in table_info and "data" in table_info:
# Default position
left = Inches(1)
top = Inches(2.5)
width = Inches(8)
height = Inches(4)
# Try to find table placeholder
for shape in slide.placeholders:
if shape.placeholder_format.type in (2, 3, 7): # Content, Object, Text
left, top, width, height = shape.left, shape.top, shape.width, shape.height
break
# Add table
rows, cols = table_info["rows"], table_info["cols"]
table = slide.shapes.add_table(
rows, cols, left, top, width, height
).table
# Add data
for r in range(min(rows, len(table_info["data"]))):
for c in range(min(cols, len(table_info["data"][r]) if r < len(table_info["data"]) else 0)):
cell = table.cell(r, c)
cell.text = str(table_info["data"][r][c])
# NLP utilities for PowerPoint content generation
class PowerPointNLP:
"""Natural language processing utilities for PowerPoint content"""
def __init__(self):
# Download required NLTK resources if not already available
try:
nltk.data.find('tokenizers/punkt')
except LookupError:
nltk.download('punkt')
try:
nltk.data.find('corpora/stopwords')
except LookupError:
nltk.download('stopwords')
def extract_slide_structure(self, text: str) -> List[Dict[str, Any]]:
"""Extract slide structure from plain text content"""
# Split text into paragraphs
paragraphs = text.split('\n\n')
# Extract title (first non-empty paragraph)
title = "New Presentation"
content_start = 0
for i, paragraph in enumerate(paragraphs):
if paragraph.strip():
title = paragraph.strip()
content_start = i + 1
break
# Initialize slides
slides = []
# Generate title slide
slides.append({
"layout": "title",
"title": title
})
# Process remaining paragraphs for content slides
current_slide = None
for paragraph in paragraphs[content_start:]:
paragraph = paragraph.strip()
if not paragraph:
continue
# Check if this is a heading (potential slide title)
sentences = sent_tokenize(paragraph)
first_sentence = sentences[0] if sentences else ""
is_heading = (
len(first_sentence) < 100 and
len(sentences) <= 2 and
not first_sentence.endswith('.')
)
if is_heading:
# Save previous slide if exists
if current_slide:
slides.append(current_slide)
# Start new slide
current_slide = {
"layout": "title_content",
"title": paragraph,
"content": ""
}
else:
# Add content to current slide or create new one if needed
if not current_slide:
current_slide = {
"layout": "title_content",
"title": "Content Slide",
"content": paragraph
}
else:
current_slide["content"] += paragraph + "\n\n"
# Add the last slide if not empty
if current_slide:
slides.append(current_slide)
return slides
def extract_structured_content(self, text: str) -> Dict[str, Any]:
"""Convert unstructured text to structured content for presentation"""
slides = self.extract_slide_structure(text)
# Create presentation structure
presentation = {
"title": slides[0]["title"] if slides else "New Presentation",
"slides": slides[1:] if slides else []
}
return presentation
def suggest_visuals(self, text: str) -> List[Dict[str, str]]:
"""Suggest visual elements based on text content"""
suggestions = []
sentences = sent_tokenize(text)
# Check for numerical data that could be visualized
numbers_pattern = r'\b\d+\b'
numbers_count = len(re.findall(numbers_pattern, text))
if numbers_count > 5:
suggestions.append({
"type": "chart",
"suggestion": "Consider adding a chart to visualize the numerical data"
})
# Check for lists that could be tables
list_markers = ['•', '-', '*', '1.', '2.', '3.']
list_items = 0
for sentence in sentences:
for marker in list_markers:
if sentence.strip().startswith(marker):
list_items += 1
break
if list_items >= 3:
suggestions.append({
"type": "table",
"suggestion": "Consider converting the list into a table for better organization"
})
# Check for comparison language
comparison_words = ['versus', 'vs', 'compared to', 'better than', 'worse than', 'difference between']
for word in comparison_words:
if word in text.lower():
suggestions.append({
"type": "comparison",
"suggestion": "Consider using a comparison slide layout with two columns"
})
break
return suggestions
# Initialize PowerPoint Manager
ppt_manager = PowerPointManager()
ppt_nlp = PowerPointNLP()
# MCP Tool implementations
@get_mcp().tool(name=PowerPointTools.CREATE_PRESENTATION)
def ppt_create_presentation(session_id: str, template_path: Optional[str] = None, ctx: Context = None) -> str:
"""Create a new PowerPoint presentation"""
return ppt_manager.create_presentation(session_id, template_path)
@get_mcp().tool(name=PowerPointTools.OPEN_PRESENTATION)
def ppt_open_presentation(session_id: str, file_path: str, ctx: Context = None) -> str:
"""Open an existing PowerPoint presentation"""
return ppt_manager.open_presentation(session_id, file_path)
@get_mcp().tool(name=PowerPointTools.SAVE_PRESENTATION)
def ppt_save_presentation(session_id: str, file_path: Optional[str] = None, ctx: Context = None) -> str:
"""Save the active PowerPoint presentation"""
return ppt_manager.save_presentation(session_id, file_path)
@get_mcp().tool(name=PowerPointTools.ADD_SLIDE)
def ppt_add_slide(session_id: str, layout_index: int = 1, title: Optional[str] = None,
content: Optional[str] = None, ctx: Context = None) -> str:
"""Add a new slide to the presentation"""
return ppt_manager.add_slide(session_id, layout_index, title, content)
@get_mcp().tool(name=PowerPointTools.ADD_TEXT)
def ppt_add_text(session_id: str, slide_index: int, text: str,
left: float = 1.0, top: float = 1.0, width: float = 8.0, height: float = 1.0,
font_size: int = 18, font_name: str = 'Calibri', bold: bool = False,
italic: bool = False, color: str = '000000', ctx: Context = None) -> str:
"""Add text box to a slide"""
return ppt_manager.add_text(session_id, slide_index, text, left, top, width, height,
font_size, font_name, bold, italic, color)
@get_mcp().tool(name=PowerPointTools.ADD_IMAGE)
def ppt_add_image(session_id: str, slide_index: int, image_path: str,
left: float = 1.0, top: float = 1.0, width: Optional[float] = None,
height: Optional[float] = None, ctx: Context = None) -> str:
"""Add image to a slide"""
return ppt_manager.add_image(session_id, slide_index, image_path, left, top, width, height)
@get_mcp().tool(name=PowerPointTools.ADD_CHART)
def ppt_add_chart(session_id: str, slide_index: int, chart_type: str,
categories: List[str], series_names: List[str], series_values: List[List[float]],
left: float = 1.0, top: float = 1.0, width: float = 8.0, height: float = 5.0,
chart_title: Optional[str] = None, ctx: Context = None) -> str:
"""Add chart to a slide"""
return ppt_manager.add_chart(session_id, slide_index, chart_type, categories, series_names,
series_values, left, top, width, height, chart_title)
@get_mcp().tool(name=PowerPointTools.ADD_TABLE)
def ppt_add_table(session_id: str, slide_index: int, rows: int, cols: int,
data: List[List[str]], left: float = 1.0, top: float = 1.0,
width: float = 8.0, height: float = 5.0, ctx: Context = None) -> str:
"""Add table to a slide"""
return ppt_manager.add_table(session_id, slide_index, rows, cols, data, left, top, width, height)
@get_mcp().tool(name=PowerPointTools.ANALYZE_PRESENTATION)
def ppt_analyze_presentation(session_id: str, ctx: Context = None) -> str:
"""Analyze the content and structure of a presentation"""
analysis = ppt_manager.analyze_presentation(session_id)
if "error" in analysis:
return analysis["error"]
return json.dumps(analysis, indent=2)
@get_mcp().tool(name=PowerPointTools.ENHANCE_PRESENTATION)
def ppt_enhance_presentation(session_id: str, ctx: Context = None) -> str:
"""Provide suggestions to enhance the presentation"""
suggestions = ppt_manager.generate_enhancement_suggestions(session_id)
if "error" in suggestions:
return suggestions["error"]
return json.dumps(suggestions, indent=2)
@get_mcp().tool(name=PowerPointTools.GENERATE_PRESENTATION)
def ppt_generate_presentation(session_id: str, title: str, content: str, ctx: Context = None) -> str:
"""Generate a presentation from text content"""
try:
# Process content with NLP
structured_content = ppt_nlp.extract_structured_content(content)
# Generate presentation
return ppt_manager.generate_presentation_from_content(session_id, title, structured_content)
except Exception as e:
return f"Error generating presentation: {str(e)}"
@get_mcp().resource("ppt://screenshots/{name}")
def get_ppt_screenshot(name: str) -> Union[MCPImage, str]:
"""Get a PowerPoint screenshot by name"""
if name in ppt_manager.screenshots:
return MCPImage(
data=ppt_manager.screenshots[name],
format="png"
)
return f"Screenshot '{name}' not found"
@get_mcp().resource("ppt://presentations")
def get_ppt_presentations() -> str:
"""Get list of active PowerPoint presentations"""
presentations = []
for session_id, session in ppt_manager.active_presentations.items():
presentations.append({
"session_id": session_id,
"file_path": session.get("file_path", "Unsaved"),
"created_at": session.get("created_at", "").isoformat() if hasattr(session.get("created_at", ""), "isoformat") else "",
"slides_count": len(session.get("presentation", {}).slides) if hasattr(session.get("presentation", {}), "slides") else 0
})
return json.dumps(presentations, indent=2)
# Natural language command processor
class PowerPointCommander:
"""Process natural language commands for PowerPoint operations"""
def __init__(self, ppt_manager, ppt_nlp):
self.ppt_manager = ppt_manager
self.ppt_nlp = ppt_nlp
self.session_id = None
def process_command(self, command: str) -> str:
"""Process a natural language command"""
command = command.lower().strip()
# Create a new presentation
if re.search(r'create|new|start', command) and re.search(r'presentation|slide deck|deck|ppt', command):
if not self.session_id:
self.session_id = f"session_{datetime.now().strftime('%Y%m%d%H%M%S')}"
template_path = None
template_match = re.search(r'template\s+(?:from|at|is|=)\s+([\w\./\\]+)', command)
if template_match:
template_path = template_match.group(1)
return self.ppt_manager.create_presentation(self.session_id, template_path)
# Open a presentation
elif re.search(r'open|load', command) and re.search(r'presentation|slide deck|deck|ppt', command):
file_match = re.search(r'(?:file|path)\s+(?:is|=)\s+([\w\./\\]+)', command)
if file_match:
file_path = file_match.group(1)
if not self.session_id:
self.session_id = f"session_{datetime.now().strftime('%Y%m%d%H%M%S')}"
return self.ppt_manager.open_presentation(self.session_id, file_path)
else:
return "Please specify the file path to open"
# Save a presentation
elif re.search(r'save', command) and re.search(r'presentation|slide deck|deck|ppt', command):
if not self.session_id:
return "No active presentation session"
file_path = None
file_match = re.search(r'(?:file|path|to)\s+(?:is|=|as)\s+([\w\./\\]+)', command)
if file_match:
file_path = file_match.group(1)
return self.ppt_manager.save_presentation(self.session_id, file_path)
# Add a slide
elif re.search(r'add', command) and re.search(r'slide', command):
if not self.session_id:
return "No active presentation session"
title = None
title_match = re.search(r'title\s+(?:is|=)\s+([\w\s]+)', command)
if title_match:
title = title_match.group(1)
content = None
content_match = re.search(r'content\s+(?:is|=)\s+([\w\s]+)', command)
if content_match:
content = content_match.group(1)
layout_index = 1
layout_match = re.search(r'layout\s+(?:is|=)\s+(\d+)', command)
if layout_match:
layout_index = int(layout_match.group(1))
return self.ppt_manager.add_slide(self.session_id, layout_index, title, content)
# Generate a presentation
elif re.search(r'generate|create|make', command) and re.search(r'from|with|using', command) and re.search(r'content|text', command):
if not self.session_id:
self.session_id = f"session_{datetime.now().strftime('%Y%m%d%H%M%S')}"
title = "Generated Presentation"
title_match = re.search(r'title\s+(?:is|=)\s+([\w\s]+)', command)
if title_match:
title = title_match.group(1)
content = "Sample content"
content_match = re.search(r'content\s+(?:is|=)\s+([\w\s]+)', command)
if content_match:
content = content_match.group(1)
structured_content = self.ppt_nlp.extract_structured_content(content)
return self.ppt_manager.generate_presentation_from_content(self.session_id, title, structured_content)
# Analyze a presentation
elif re.search(r'analyze|review', command) and re.search(r'presentation|slide deck|deck|ppt', command):
if not self.session_id:
return "No active presentation session"
analysis = self.ppt_manager.analyze_presentation(self.session_id)
if "error" in analysis:
return analysis["error"]
return json.dumps(analysis, indent=2)
# Enhance a presentation
elif re.search(r'enhance|improve|suggest', command) and re.search(r'presentation|slide deck|deck|ppt', command):
if not self.session_id:
return "No active presentation session"
suggestions = self.ppt_manager.generate_enhancement_suggestions(self.session_id)
if "error" in suggestions:
return suggestions["error"]
return json.dumps(suggestions, indent=2)
else:
return "Unrecognized command. Try something like 'create new presentation', 'add slide', or 'analyze presentation'."
# Initialize the commander
ppt_commander = PowerPointCommander(ppt_manager, ppt_nlp)
# Add a natural language command processor tool
@get_mcp().tool(name="ppt_command")
def ppt_command(command: str, ctx: Context = None) -> str:
"""Process a natural language command for PowerPoint operations"""
return ppt_commander.process_command(command)
def get_ppt_tools():
"""Return the PowerPoint tools for registration with another MCP instance"""
return {
PowerPointTools.CREATE_PRESENTATION: ppt_create_presentation,
PowerPointTools.OPEN_PRESENTATION: ppt_open_presentation,
PowerPointTools.SAVE_PRESENTATION: ppt_save_presentation,
PowerPointTools.ADD_SLIDE: ppt_add_slide,
PowerPointTools.ADD_TEXT: ppt_add_text,
PowerPointTools.ADD_IMAGE: ppt_add_image,
PowerPointTools.ADD_CHART: ppt_add_chart,
PowerPointTools.ADD_TABLE: ppt_add_table,
PowerPointTools.ANALYZE_PRESENTATION: ppt_analyze_presentation,
PowerPointTools.ENHANCE_PRESENTATION: ppt_enhance_presentation,
PowerPointTools.GENERATE_PRESENTATION: ppt_generate_presentation,
"ppt_command": ppt_command
}
def set_external_mcp(external_mcp_instance):
"""Set an external MCP instance to use instead of the local one"""
global _external_mcp
global mcp
_external_mcp = external_mcp_instance
mcp = get_mcp()
return _external_mcp
# Main execution as MCP Tool
if __name__ == "__main__":
# Use configuration from environment variables if available
host = os.environ.get("MCP_HOST", "0.0.0.0")
port = int(os.environ.get("MCP_PORT", "8000"))
log_level = os.environ.get("MCP_LOG_LEVEL", "debug")
# Update configuration
mcp.config = {
"host": host,
"port": port,
"log_level": log_level
}
# Run the server with configuration from mcp.config
mcp.run()