BruceWayne1 commited on
Commit
6ded552
·
verified ·
1 Parent(s): 015d23b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +435 -561
app.py CHANGED
@@ -1,7 +1,8 @@
 
1
  #!/usr/bin/env python
2
  """
3
- PowerPoint MCP Server - Fixed Complete Implementation
4
- Supports both MCP STDIO transport and Gradio web interface
5
  """
6
  import os
7
  import json
@@ -11,417 +12,367 @@ import uuid
11
  import logging
12
  import gradio as gr
13
  from threading import Thread
14
- from typing import Dict, Any, Optional, List
 
15
  import asyncio
16
- import sys
17
-
 
 
 
 
 
 
 
 
 
 
 
18
  # Set up logging
19
  logging.basicConfig(level=logging.INFO)
20
  logger = logging.getLogger(__name__)
21
-
22
- # Global state for presentations
 
 
 
23
  presentations = {}
24
  current_presentation_id = None
25
-
26
- # Check for dependencies
27
- MCP_AVAILABLE = False
28
- PPTX_AVAILABLE = False
29
-
30
- # PowerPoint imports
31
- try:
32
- from pptx import Presentation
33
- from pptx.util import Inches, Pt
34
- from pptx.enum.text import PP_ALIGN
35
- from pptx.enum.shapes import MSO_SHAPE
36
- from pptx.enum.chart import XL_CHART_TYPE
37
- PPTX_AVAILABLE = True
38
- print("✅ python-pptx loaded successfully")
39
- except ImportError:
40
- PPTX_AVAILABLE = False
41
- print("❌ python-pptx not available. Install with: pip install python-pptx")
42
-
43
- # MCP imports
44
- try:
45
- from mcp.server import Server
46
- from mcp.server.stdio import stdio_server
47
- import mcp.types as types
48
- MCP_AVAILABLE = True
49
- print("✅ MCP loaded successfully")
50
- except ImportError:
51
- MCP_AVAILABLE = False
52
- print("❌ MCP not available. Install with: pip install mcp")
53
-
54
- # Always initialize server (even if MCP not available for web mode)
55
- server = None
56
- if MCP_AVAILABLE:
57
- server = Server("powerpoint-mcp-server")
58
-
59
- # Helper Functions
60
  def get_current_presentation():
61
- """Get the current presentation or create a new one."""
62
- global current_presentation_id
63
  if current_presentation_id is None or current_presentation_id not in presentations:
64
- # Create a default presentation
65
- if PPTX_AVAILABLE:
66
- prs = Presentation()
67
- current_presentation_id = str(uuid.uuid4())
68
- presentations[current_presentation_id] = prs
69
- else:
70
- raise ValueError("python-pptx not available")
71
  return presentations[current_presentation_id]
72
-
73
- def create_presentation_sync(title="New Presentation"):
74
- """Create a new presentation synchronously."""
75
- if not PPTX_AVAILABLE:
76
- return None, "python-pptx not available"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  try:
 
79
  prs = Presentation()
80
  presentation_id = str(uuid.uuid4())
81
  presentations[presentation_id] = prs
 
82
 
83
- global current_presentation_id
84
- current_presentation_id = presentation_id
85
-
86
- logger.info(f"Created presentation '{title}' with ID: {presentation_id}")
87
- return presentation_id, f"Created presentation '{title}' with ID: {presentation_id}"
88
  except Exception as e:
89
- logger.error(f"Error creating presentation: {str(e)}")
90
- return None, f"Error creating presentation: {str(e)}"
91
-
92
- def add_slide_sync(title="New Slide", content="", layout_idx=1):
93
- """Add a slide synchronously."""
94
  try:
95
- prs = get_current_presentation()
96
- slide_layout = prs.slide_layouts[layout_idx]
 
 
 
 
 
97
  slide = prs.slides.add_slide(slide_layout)
98
 
99
- # Set title if available
100
  if slide.shapes.title:
101
  slide.shapes.title.text = title
102
 
103
- # Set content if available and layout supports it
104
- if content and len(slide.placeholders) > 1:
105
  slide.placeholders[1].text = content
106
 
107
- slide_number = len(prs.slides)
108
- logger.info(f"Added slide {slide_number}: '{title}'")
109
- return True, f"Added slide {slide_number}: '{title}'"
110
  except Exception as e:
111
- logger.error(f"Error adding slide: {str(e)}")
112
  return False, f"Error adding slide: {str(e)}"
113
-
114
- def save_presentation_sync(filename=None):
115
- """Save presentation synchronously."""
116
  try:
117
- if not current_presentation_id or current_presentation_id not in presentations:
118
- return None, "No active presentation to save"
119
-
120
- prs = presentations[current_presentation_id]
121
-
122
- if filename is None:
123
- filename = f"presentation_{current_presentation_id[:8]}.pptx"
124
 
125
- if not filename.endswith('.pptx'):
126
- filename += '.pptx'
 
127
 
128
- # Save to temp directory for web access
129
- temp_dir = tempfile.gettempdir()
130
- filepath = os.path.join(temp_dir, filename)
131
- prs.save(filepath)
132
-
133
- logger.info(f"Presentation saved to: {filepath}")
134
- return filepath, f"Presentation saved to: {filepath}"
135
  except Exception as e:
136
- logger.error(f"Error saving presentation: {str(e)}")
137
- return None, f"Error saving presentation: {str(e)}"
138
-
139
- # MCP Server Implementation - Only if MCP is available
140
- if MCP_AVAILABLE and server is not None:
141
- @server.list_tools()
142
- async def handle_list_tools() -> List[types.Tool]:
143
- """List all available MCP tools."""
144
- return [
145
- types.Tool(
146
- name="create_presentation",
147
- description="Create a new PowerPoint presentation",
148
- inputSchema={
149
- "type": "object",
150
- "properties": {
151
- "title": {
152
- "type": "string",
153
- "description": "Title for the presentation",
154
- "default": "New Presentation"
155
- }
156
- }
157
- }
158
- ),
159
- types.Tool(
160
- name="add_slide",
161
- description="Add a new slide to the current presentation",
162
- inputSchema={
163
- "type": "object",
164
- "properties": {
165
- "title": {
166
- "type": "string",
167
- "description": "Title for the slide"
168
- },
169
- "content": {
170
- "type": "string",
171
- "description": "Content/body text for the slide"
172
- },
173
- "layout": {
174
- "type": "integer",
175
- "description": "Layout index (0=Title, 1=Title+Content, 2=Section Header, etc.)",
176
- "default": 1
177
- }
178
- },
179
- "required": ["title"]
180
- }
181
- ),
182
- types.Tool(
183
- name="save_presentation",
184
- description="Save the current presentation to a file",
185
- inputSchema={
186
- "type": "object",
187
- "properties": {
188
- "filename": {
189
- "type": "string",
190
- "description": "Filename for the presentation (without extension)"
191
- }
192
- }
193
- }
194
- ),
195
- types.Tool(
196
- name="get_presentation_info",
197
- description="Get information about the current presentation",
198
- inputSchema={"type": "object", "properties": {}}
199
- ),
200
- types.Tool(
201
- name="add_bullet_slide",
202
- description="Add a slide with bullet points",
203
- inputSchema={
204
- "type": "object",
205
- "properties": {
206
- "title": {"type": "string", "description": "Slide title"},
207
- "bullets": {
208
- "type": "array",
209
- "items": {"type": "string"},
210
- "description": "List of bullet points"
211
- }
212
- },
213
- "required": ["title", "bullets"]
214
- }
215
- ),
216
- types.Tool(
217
- name="create_presentation_from_outline",
218
- description="Create a complete presentation from a structured outline",
219
- inputSchema={
220
- "type": "object",
221
- "properties": {
222
- "title": {"type": "string", "description": "Presentation title"},
223
- "slides": {
224
- "type": "array",
225
- "items": {
226
- "type": "object",
227
- "properties": {
228
- "title": {"type": "string"},
229
- "content": {"type": "string"},
230
- "type": {"type": "string", "enum": ["title", "content", "bullets", "section"], "default": "content"}
231
- }
232
- }
233
- }
234
- },
235
- "required": ["title", "slides"]
236
- }
237
- )
238
- ]
239
-
240
- @server.call_tool()
241
- async def handle_call_tool(name: str, arguments: dict) -> List[types.TextContent]:
242
- """Handle tool execution."""
243
- try:
244
- if not PPTX_AVAILABLE:
245
- return [types.TextContent(
246
- type="text",
247
- text="Error: python-pptx library not available. Please install it with: pip install python-pptx"
248
- )]
249
-
250
- if name == "create_presentation":
251
- return await create_presentation_tool(arguments)
252
- elif name == "add_slide":
253
- return await add_slide_tool(arguments)
254
- elif name == "save_presentation":
255
- return await save_presentation_tool(arguments)
256
- elif name == "get_presentation_info":
257
- return await get_presentation_info_tool(arguments)
258
- elif name == "add_bullet_slide":
259
- return await add_bullet_slide_tool(arguments)
260
- elif name == "create_presentation_from_outline":
261
- return await create_presentation_from_outline_tool(arguments)
262
- else:
263
- return [types.TextContent(type="text", text=f"Unknown tool: {name}")]
264
-
265
- except Exception as e:
266
- logger.error(f"Error in tool {name}: {str(e)}")
267
- return [types.TextContent(type="text", text=f"Error executing {name}: {str(e)}")]
268
-
269
- # Tool implementations
270
- async def create_presentation_tool(args: dict) -> List[types.TextContent]:
271
- """Create a new presentation tool."""
272
- title = args.get("title", "New Presentation")
273
- presentation_id, message = create_presentation_sync(title)
274
- return [types.TextContent(type="text", text=message)]
275
-
276
- async def add_slide_tool(args: dict) -> List[types.TextContent]:
277
- """Add slide tool."""
278
- title = args.get("title", "New Slide")
279
- content = args.get("content", "")
280
- layout_idx = args.get("layout", 1)
281
-
282
- success, message = add_slide_sync(title, content, layout_idx)
283
- return [types.TextContent(type="text", text=message)]
284
-
285
- async def save_presentation_tool(args: dict) -> List[types.TextContent]:
286
- """Save presentation tool."""
287
- filename = args.get("filename")
288
- filepath, message = save_presentation_sync(filename)
289
- return [types.TextContent(type="text", text=message)]
290
-
291
- async def get_presentation_info_tool(args: dict) -> List[types.TextContent]:
292
- """Get presentation info tool."""
293
- try:
294
- if not current_presentation_id or current_presentation_id not in presentations:
295
- return [types.TextContent(type="text", text="No active presentation")]
296
-
297
- prs = presentations[current_presentation_id]
298
- info = {
299
- "presentation_id": current_presentation_id,
300
- "slide_count": len(prs.slides),
301
- "slide_layouts_available": len(prs.slide_layouts),
302
- "core_properties": {
303
- "title": prs.core_properties.title or "Untitled",
304
- "author": prs.core_properties.author or "Unknown",
305
- "subject": prs.core_properties.subject or "",
306
- }
307
- }
308
-
309
- return [types.TextContent(type="text", text=json.dumps(info, indent=2))]
310
- except Exception as e:
311
- return [types.TextContent(type="text", text=f"Error getting info: {str(e)}")]
312
-
313
- async def add_bullet_slide_tool(args: dict) -> List[types.TextContent]:
314
- """Add a slide with bullet points."""
315
- try:
316
- prs = get_current_presentation()
317
- slide_layout = prs.slide_layouts[1] # Title and content layout
318
- slide = prs.slides.add_slide(slide_layout)
319
-
320
- title = args.get("title", "Bullet Points")
321
- bullets = args.get("bullets", [])
322
-
323
- # Set title
324
- if slide.shapes.title:
325
- slide.shapes.title.text = title
326
-
327
- # Add bullet points
328
- if len(slide.placeholders) > 1 and bullets:
329
- text_frame = slide.placeholders[1].text_frame
330
- text_frame.text = bullets[0] # First bullet
331
-
332
- # Add additional bullets
333
- for bullet in bullets[1:]:
334
- p = text_frame.add_paragraph()
335
- p.text = bullet
336
- p.level = 0 # Main bullet level
337
-
338
- return [types.TextContent(type="text", text=f"Added bullet slide: '{title}' with {len(bullets)} points")]
339
- except Exception as e:
340
- return [types.TextContent(type="text", text=f"Error adding bullet slide: {str(e)}")]
341
-
342
- async def create_presentation_from_outline_tool(args: dict) -> List[types.TextContent]:
343
- """Create a complete presentation from outline."""
344
- try:
345
- # Create new presentation
346
- title = args.get("title", "New Presentation")
347
- presentation_id, create_msg = create_presentation_sync(title)
348
-
349
- if not presentation_id:
350
- return [types.TextContent(type="text", text=create_msg)]
351
-
352
- slides_data = args.get("slides", [])
353
- created_slides = 0
354
-
355
- for slide_info in slides_data:
356
- slide_title = slide_info.get("title", "Untitled Slide")
357
- slide_content = slide_info.get("content", "")
358
- slide_type = slide_info.get("type", "content")
359
-
360
- if slide_type == "title":
361
- success, msg = add_slide_sync(slide_title, slide_content, 0) # Title layout
362
- elif slide_type == "section":
363
- success, msg = add_slide_sync(slide_title, slide_content, 2) # Section header
364
- elif slide_type == "bullets":
365
- # Convert content to bullet points
366
- bullets = slide_content.split('\n') if slide_content else ["Bullet point"]
367
- result = await add_bullet_slide_tool({"title": slide_title, "bullets": bullets})
368
- success = True
369
- else: # content
370
- success, msg = add_slide_sync(slide_title, slide_content, 1) # Title and content
371
-
372
- if success:
373
- created_slides += 1
374
-
375
- # Save the presentation
376
- filepath, save_msg = save_presentation_sync(f"{title.replace(' ', '_')}")
377
-
378
- final_message = f"Created presentation '{title}' with {created_slides} slides. {save_msg}"
379
- return [types.TextContent(type="text", text=final_message)]
380
-
381
- except Exception as e:
382
- return [types.TextContent(type="text", text=f"Error creating presentation from outline: {str(e)}")]
383
-
384
  # Gradio Interface Functions
385
- def create_presentation_gradio(title):
386
- """Create presentation for Gradio interface."""
387
- if not title:
388
- title = "New Presentation"
389
-
390
- presentation_id, message = create_presentation_sync(title)
391
- return f"✅ {message}" if presentation_id else f"❌ {message}"
392
-
393
- def add_slide_gradio(title, content):
394
- """Add slide for Gradio interface."""
395
- if not title:
396
- return "❌ Please provide a slide title"
397
-
398
- success, message = add_slide_sync(title, content or "")
399
- return f"✅ {message}" if success else f"❌ {message}"
400
-
401
- def create_from_json_gradio(json_data):
402
- """Create presentation from JSON data."""
403
  try:
404
- if isinstance(json_data, str):
405
- data = json.loads(json_data)
406
  else:
407
- data = json_data
408
 
409
  # Create presentation
410
- title = data.get("title", "JSON Presentation")
411
- presentation_id, create_message = create_presentation_sync(title)
412
-
413
  if not presentation_id:
414
- return None, f"❌ {create_message}"
415
 
416
  # Add slides
417
- slides_data = data.get("slides", [])
418
  slide_count = 0
419
-
420
- for slide_info in slides_data:
421
- slide_title = slide_info.get("title", f"Slide {slide_count + 1}")
422
- slide_content = slide_info.get("content", "")
423
-
424
- success, message = add_slide_sync(slide_title, slide_content)
425
  if success:
426
  slide_count += 1
427
 
@@ -429,235 +380,158 @@ def create_from_json_gradio(json_data):
429
  return None, "❌ No slides were created successfully"
430
 
431
  # Save presentation
432
- filename = f"{title.replace(' ', '_')}_gradio"
433
- filepath, save_message = save_presentation_sync(filename)
434
 
435
- if filepath:
436
- return filepath, f"✅ Created presentation with {slide_count} slides. {save_message}"
437
  else:
438
- return None, f"❌ {save_message}"
439
 
440
- except json.JSONDecodeError as e:
441
- return None, f"❌ Invalid JSON: {str(e)}"
442
  except Exception as e:
 
443
  return None, f"❌ Error: {str(e)}"
444
-
445
  def get_server_status():
446
- """Get current server status."""
447
- mcp_status = "✅ Available" if MCP_AVAILABLE else "❌ Not installed"
448
- pptx_status = "✅ Available" if PPTX_AVAILABLE else "❌ Not installed"
449
-
450
  return f"""
451
  🟢 **Server Status**: Running
452
- 📦 **MCP Library**: {mcp_status}
453
- 📊 **PowerPoint Library**: {pptx_status}
454
- 🔧 **Transport**: STDIO
455
- ⚡ **Tools Available**: 6 (create_presentation, add_slide, save_presentation, etc.)
456
  📊 **Presentations Loaded**: {len(presentations)}
457
- 🎮 **Current Presentation**: {current_presentation_id[-8:] if current_presentation_id else 'None'}
458
- 📁 **Temp Directory**: {tempfile.gettempdir()}
459
- """
460
-
461
- # Gradio Interface
462
  def create_gradio_interface():
463
- """Create the Gradio web interface."""
464
 
465
- with gr.Blocks(title="PowerPoint MCP Server", theme=gr.themes.Soft()) as demo:
466
  gr.Markdown("# 🎯 PowerPoint MCP Server")
467
- gr.Markdown("**Complete MCP implementation with PowerPoint generation capabilities**")
468
 
469
- with gr.Tab("🚀 Quick Test"):
470
  with gr.Row():
471
  with gr.Column():
472
- gr.Markdown("### Create New Presentation")
473
- title_input = gr.Textbox(
474
- label="Presentation Title",
475
- value="My Test Presentation",
476
- placeholder="Enter presentation title"
 
477
  )
478
- create_btn = gr.Button("📁 Create Presentation", variant="primary")
479
- create_output = gr.Textbox(label="Status", interactive=False)
480
 
481
  with gr.Column():
482
- gr.Markdown("### Add Slide")
483
- slide_title = gr.Textbox(
484
- label="Slide Title",
485
- value="Welcome",
486
- placeholder="Enter slide title"
487
  )
488
- slide_content = gr.Textbox(
489
- label="Slide Content",
490
- value="This is my first slide content",
491
- lines=3,
492
- placeholder="Enter slide content"
493
- )
494
- add_slide_btn = gr.Button("➕ Add Slide", variant="secondary")
495
- slide_output = gr.Textbox(label="Status", interactive=False)
496
-
497
- with gr.Row():
498
- save_btn = gr.Button("💾 Save Presentation", variant="primary", size="lg")
499
- download_btn = gr.DownloadButton("📥 Download PowerPoint", visible=False, size="lg")
500
- save_output = gr.Textbox(label="Save Status", interactive=False)
501
-
502
- with gr.Tab("📝 JSON Creator"):
503
- with gr.Row():
504
- with gr.Column():
505
- gr.Markdown("### Create from JSON Structure")
506
- json_input = gr.Textbox(
507
- label="JSON Data",
508
- value='{\n "title": "Sample Presentation",\n "slides": [\n {\n "title": "Introduction",\n "content": "Welcome to our presentation"\n },\n {\n "title": "Main Points",\n "content": "Point 1\\nPoint 2\\nPoint 3"\n },\n {\n "title": "Conclusion",\n "content": "Thank you for your attention"\n }\n ]\n}',
509
- lines=15,
510
- placeholder='{"title": "My Presentation", "slides": [...]}'
511
- )
512
- json_create_btn = gr.Button("🚀 Create from JSON", variant="primary", size="lg")
513
-
514
- with gr.Column():
515
- json_output = gr.Textbox(
516
- label="Creation Status",
517
- lines=5,
518
- interactive=False
519
- )
520
- json_download_btn = gr.DownloadButton(
521
- "📥 Download JSON Presentation",
522
  visible=False,
523
  size="lg"
524
  )
525
 
526
  with gr.Tab("🔧 MCP Configuration"):
527
- gr.Markdown("## MCP Server Configuration")
528
 
529
  with gr.Row():
530
  with gr.Column():
531
- gr.Markdown("### For Claude Desktop")
532
- claude_config = '''{
533
- "mcpServers": {
534
- "powerpoint": {
535
- "command": "python",
536
- "args": ["path/to/your/app.py", "mcp"],
537
- "env": {
538
- "MCP_MODE": "stdio"
539
- }
540
- }
541
- }
542
- }'''
543
- gr.Code(claude_config, language="json")
544
 
545
  with gr.Column():
546
- gr.Markdown("### Command Line Usage")
547
- commands = '''# Run as MCP server
548
- python app.py mcp
549
-
550
- # Or with environment variable
551
- MCP_MODE=stdio python app.py
552
-
553
- # Run web interface (current mode)
554
- python app.py'''
555
- gr.Code(commands, language="shell")
556
-
557
- gr.Markdown("### Available MCP Tools")
558
- tools_list = """
559
- - **create_presentation**: Create a new PowerPoint presentation
560
- - **add_slide**: Add slides with title and content
561
- - **save_presentation**: Save presentation to file
562
- - **get_presentation_info**: Get presentation details
563
- - **add_bullet_slide**: Add slide with bullet points
564
- - **create_presentation_from_outline**: Create complete presentation from structured data
565
- """
566
- gr.Markdown(tools_list)
567
 
568
  with gr.Tab("📊 Server Status"):
569
- status_display = gr.Textbox(
570
  value=get_server_status(),
571
  label="Server Information",
572
- lines=12,
573
  interactive=False
574
  )
575
- refresh_btn = gr.Button("🔄 Refresh Status")
576
- refresh_btn.click(fn=get_server_status, outputs=[status_display])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
577
 
578
  # Event handlers
579
  create_btn.click(
580
- fn=create_presentation_gradio,
581
- inputs=[title_input],
582
- outputs=[create_output]
583
- )
584
-
585
- add_slide_btn.click(
586
- fn=add_slide_gradio,
587
- inputs=[slide_title, slide_content],
588
- outputs=[slide_output]
589
- )
590
-
591
- def save_and_prepare_download():
592
- filepath, message = save_presentation_sync()
593
- if filepath:
594
- return filepath, message, gr.update(visible=True)
595
- else:
596
- return None, message, gr.update(visible=False)
597
-
598
- save_btn.click(
599
- fn=save_and_prepare_download,
600
- outputs=[download_btn, save_output, download_btn]
601
- )
602
-
603
- json_create_btn.click(
604
- fn=create_from_json_gradio,
605
- inputs=[json_input],
606
- outputs=[json_download_btn, json_output]
607
  ).then(
608
  fn=lambda x: gr.update(visible=bool(x)),
609
- inputs=[json_download_btn],
610
- outputs=[json_download_btn]
611
  )
612
 
613
  return demo
614
-
615
- # MCP Server runner
616
- async def run_mcp_server():
617
- """Run the MCP server with STDIO transport."""
618
- if not MCP_AVAILABLE or server is None:
619
- print("Error: MCP library not available or server not initialized.")
620
- return
621
-
622
- async with stdio_server() as (read_stream, write_stream):
623
- await server.run(
624
- read_stream,
625
- write_stream,
626
- server.create_initialization_options()
627
- )
628
-
629
  # Main execution
630
- def main():
631
- """Main entry point."""
632
- print(f"🔍 Dependency Check:")
633
- print(f" - MCP: {'✅' if MCP_AVAILABLE else '❌'}")
634
- print(f" - python-pptx: {'✅' if PPTX_AVAILABLE else '❌'}")
635
-
636
- # Check command line arguments and environment
637
- if len(sys.argv) > 1 and sys.argv[1] == "mcp":
638
- # Run as MCP server
639
- print("Starting PowerPoint MCP Server with STDIO transport...")
640
- if not MCP_AVAILABLE:
641
- print("Error: MCP library not available. Install with: pip install mcp")
642
- sys.exit(1)
643
- asyncio.run(run_mcp_server())
644
- elif os.getenv("MCP_MODE") == "stdio":
645
- # Run as MCP server via environment variable
646
- print("Starting PowerPoint MCP Server (via MCP_MODE env var)...")
647
- if not MCP_AVAILABLE:
648
- print("Error: MCP library not available. Install with: pip install mcp")
649
- sys.exit(1)
650
- asyncio.run(run_mcp_server())
651
- else:
652
- # Run Gradio web interface
653
- print("Starting PowerPoint MCP Server with Gradio web interface...")
654
- demo = create_gradio_interface()
655
- demo.launch(
656
- server_name="0.0.0.0",
657
- server_port=7860,
658
- share=True,
659
- show_error=True
660
- )
661
-
662
  if __name__ == "__main__":
663
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
  #!/usr/bin/env python
3
  """
4
+ PowerPoint MCP Server with Gradio interface and Streamable HTTP transport
5
+ Integrated with existing tool structure from ppt_mcp_server.py
6
  """
7
  import os
8
  import json
 
12
  import logging
13
  import gradio as gr
14
  from threading import Thread
15
+ from typing import Dict, Any, Optional
16
+ from mcp.server.fastmcp import FastMCP
17
  import asyncio
18
+ # Import all your existing tools registration functions
19
+ from tools import (
20
+ register_presentation_tools,
21
+ register_content_tools,
22
+ register_structural_tools,
23
+ register_professional_tools,
24
+ register_template_tools,
25
+ register_hyperlink_tools,
26
+ register_chart_tools,
27
+ register_connector_tools,
28
+ register_master_tools,
29
+ register_transition_tools
30
+ )
31
  # Set up logging
32
  logging.basicConfig(level=logging.INFO)
33
  logger = logging.getLogger(__name__)
34
+ # Initialize the FastMCP server for streamable HTTP
35
+ mcp_app = FastMCP(
36
+ name="ppt-mcp-server"
37
+ )
38
+ # Global state to store presentations in memory (from original)
39
  presentations = {}
40
  current_presentation_id = None
41
+ # Template configuration (from original)
42
+ def get_template_search_directories():
43
+ """
44
+ Get list of directories to search for templates.
45
+ Uses environment variable PPT_TEMPLATE_PATH if set, otherwise uses default directories.
46
+ """
47
+ template_env_path = os.environ.get('PPT_TEMPLATE_PATH')
48
+
49
+ if template_env_path:
50
+ import platform
51
+ separator = ';' if platform.system() == "Windows" else ':'
52
+ env_dirs = [path.strip() for path in template_env_path.split(separator) if path.strip()]
53
+
54
+ valid_env_dirs = []
55
+ for dir_path in env_dirs:
56
+ expanded_path = os.path.expanduser(dir_path)
57
+ if os.path.exists(expanded_path) and os.path.isdir(expanded_path):
58
+ valid_env_dirs.append(expanded_path)
59
+
60
+ if valid_env_dirs:
61
+ return valid_env_dirs + ['.', './templates', './assets', './resources']
62
+ else:
63
+ print(f"Warning: PPT_TEMPLATE_PATH directories not found: {template_env_path}")
64
+
65
+ return ['.', './templates', './assets', './resources']
66
+ # Helper Functions (from original)
 
 
 
 
 
 
 
 
 
67
  def get_current_presentation():
68
+ """Get the current presentation object or raise an error if none is loaded."""
 
69
  if current_presentation_id is None or current_presentation_id not in presentations:
70
+ raise ValueError("No presentation is currently loaded. Please create or open a presentation first.")
 
 
 
 
 
 
71
  return presentations[current_presentation_id]
72
+ def get_current_presentation_id():
73
+ """Get the current presentation ID."""
74
+ return current_presentation_id
75
+ def set_current_presentation_id(pres_id):
76
+ """Set the current presentation ID."""
77
+ global current_presentation_id
78
+ current_presentation_id = pres_id
79
+ def validate_parameters(params):
80
+ """Validate parameters against constraints."""
81
+ for param_name, (value, constraints) in params.items():
82
+ for constraint_func, error_msg in constraints:
83
+ if not constraint_func(value):
84
+ return False, f"Parameter '{param_name}': {error_msg}"
85
+ return True, None
86
+ def is_positive(value):
87
+ """Check if a value is positive."""
88
+ return value > 0
89
+ def is_non_negative(value):
90
+ """Check if a value is non-negative."""
91
+ return value >= 0
92
+ def is_in_range(min_val, max_val):
93
+ """Create a function that checks if a value is in a range."""
94
+ return lambda x: min_val <= x <= max_val
95
+ def is_in_list(valid_list):
96
+ """Create a function that checks if a value is in a list."""
97
+ return lambda x: x in valid_list
98
+ def is_valid_rgb(color_list):
99
+ """Check if a color list is a valid RGB tuple."""
100
+ if not isinstance(color_list, list) or len(color_list) != 3:
101
+ return False
102
+ return all(isinstance(c, int) and 0 <= c <= 255 for c in color_list)
103
+ def add_shape_direct(slide, shape_type: str, left: float, top: float, width: float, height: float) -> Any:
104
+ """Add an auto shape to a slide using direct integer values."""
105
+ from pptx.util import Inches
106
+
107
+ shape_type_map = {
108
+ 'rectangle': 1,
109
+ 'rounded_rectangle': 2,
110
+ 'oval': 9,
111
+ 'diamond': 4,
112
+ 'triangle': 5,
113
+ 'right_triangle': 6,
114
+ 'pentagon': 56,
115
+ 'hexagon': 10,
116
+ 'heptagon': 11,
117
+ 'octagon': 12,
118
+ 'star': 12,
119
+ 'arrow': 13,
120
+ 'cloud': 35,
121
+ 'heart': 21,
122
+ 'lightning_bolt': 22,
123
+ 'sun': 23,
124
+ 'moon': 24,
125
+ 'smiley_face': 17,
126
+ 'no_symbol': 19,
127
+ 'flowchart_process': 112,
128
+ 'flowchart_decision': 114,
129
+ 'flowchart_data': 115,
130
+ 'flowchart_document': 119
131
+ }
132
+
133
+ shape_type_lower = str(shape_type).lower()
134
+ if shape_type_lower not in shape_type_map:
135
+ available_shapes = ', '.join(sorted(shape_type_map.keys()))
136
+ raise ValueError(f"Unsupported shape type: '{shape_type}'. Available shape types: {available_shapes}")
137
+
138
+ shape_value = shape_type_map[shape_type_lower]
139
+
140
+ try:
141
+ shape = slide.shapes.add_shape(
142
+ shape_value, Inches(left), Inches(top), Inches(width), Inches(height)
143
+ )
144
+ return shape
145
+ except Exception as e:
146
+ raise ValueError(f"Failed to create '{shape_type}' shape using direct value {shape_value}: {str(e)}")
147
+ # Custom presentation management wrapper (from original)
148
+ class PresentationManager:
149
+ """Wrapper to handle presentation state updates."""
150
+
151
+ def __init__(self, presentations_dict):
152
+ self.presentations = presentations_dict
153
+
154
+ def store_presentation(self, pres, pres_id):
155
+ """Store a presentation and set it as current."""
156
+ self.presentations[pres_id] = pres
157
+ set_current_presentation_id(pres_id)
158
+ return pres_id
159
+ # Create presentation manager wrapper
160
+ presentation_manager = PresentationManager(presentations)
161
+ # Register all tool modules (from original)
162
+ register_presentation_tools(
163
+ mcp_app,
164
+ presentations,
165
+ get_current_presentation_id,
166
+ get_template_search_directories
167
+ )
168
+ register_content_tools(
169
+ mcp_app,
170
+ presentations,
171
+ get_current_presentation_id,
172
+ validate_parameters,
173
+ is_positive,
174
+ is_non_negative,
175
+ is_in_range,
176
+ is_valid_rgb
177
+ )
178
+ register_structural_tools(
179
+ mcp_app,
180
+ presentations,
181
+ get_current_presentation_id,
182
+ validate_parameters,
183
+ is_positive,
184
+ is_non_negative,
185
+ is_in_range,
186
+ is_valid_rgb,
187
+ add_shape_direct
188
+ )
189
+ register_professional_tools(
190
+ mcp_app,
191
+ presentations,
192
+ get_current_presentation_id
193
+ )
194
+ register_template_tools(
195
+ mcp_app,
196
+ presentations,
197
+ get_current_presentation_id
198
+ )
199
+ register_hyperlink_tools(
200
+ mcp_app,
201
+ presentations,
202
+ get_current_presentation_id,
203
+ validate_parameters,
204
+ is_positive,
205
+ is_non_negative,
206
+ is_in_range,
207
+ is_valid_rgb
208
+ )
209
+ register_chart_tools(
210
+ mcp_app,
211
+ presentations,
212
+ get_current_presentation_id,
213
+ validate_parameters,
214
+ is_positive,
215
+ is_non_negative,
216
+ is_in_range,
217
+ is_valid_rgb
218
+ )
219
+ register_connector_tools(
220
+ mcp_app,
221
+ presentations,
222
+ get_current_presentation_id,
223
+ validate_parameters,
224
+ is_positive,
225
+ is_non_negative,
226
+ is_in_range,
227
+ is_valid_rgb
228
+ )
229
+ register_master_tools(
230
+ mcp_app,
231
+ presentations,
232
+ get_current_presentation_id,
233
+ validate_parameters,
234
+ is_positive,
235
+ is_non_negative,
236
+ is_in_range,
237
+ is_valid_rgb
238
+ )
239
+ register_transition_tools(
240
+ mcp_app,
241
+ presentations,
242
+ get_current_presentation_id,
243
+ validate_parameters,
244
+ is_positive,
245
+ is_non_negative,
246
+ is_in_range,
247
+ is_valid_rgb
248
+ )
249
+ # Additional Utility Tools (from original)
250
+ @mcp_app.tool()
251
+ def list_presentations() -> Dict:
252
+ """List all loaded presentations."""
253
+ return {
254
+ "presentations": [
255
+ {
256
+ "id": pres_id,
257
+ "slide_count": len(pres.slides),
258
+ "is_current": pres_id == current_presentation_id
259
+ }
260
+ for pres_id, pres in presentations.items()
261
+ ],
262
+ "current_presentation_id": current_presentation_id,
263
+ "total_presentations": len(presentations)
264
+ }
265
+ @mcp_app.tool()
266
+ def switch_presentation(presentation_id: str) -> Dict:
267
+ """Switch to a different loaded presentation."""
268
+ if presentation_id not in presentations:
269
+ return {
270
+ "error": f"Presentation '{presentation_id}' not found. Available presentations: {list(presentations.keys())}"
271
+ }
272
+
273
+ global current_presentation_id
274
+ old_id = current_presentation_id
275
+ current_presentation_id = presentation_id
276
+
277
+ return {
278
+ "message": f"Switched from presentation '{old_id}' to '{presentation_id}'",
279
+ "previous_presentation_id": old_id,
280
+ "current_presentation_id": current_presentation_id
281
+ }
282
+ @mcp_app.tool()
283
+ def get_server_info() -> Dict:
284
+ """Get information about the MCP server."""
285
+ return {
286
+ "name": "PowerPoint MCP Server - Enhanced Edition",
287
+ "version": "2.1.0",
288
+ "total_tools": 32,
289
+ "loaded_presentations": len(presentations),
290
+ "current_presentation": current_presentation_id,
291
+ "transport": "streamable-http",
292
+ "features": [
293
+ "Presentation Management (7 tools)",
294
+ "Content Management (6 tools)",
295
+ "Template Operations (7 tools)",
296
+ "Structural Elements (4 tools)",
297
+ "Professional Design (3 tools)",
298
+ "Specialized Features (5 tools)"
299
+ ]
300
+ }
301
+ # Direct tool usage functions for Gradio (using internal MCP mechanisms)
302
+ def create_test_presentation():
303
+ """Create a test presentation using MCP tools directly"""
304
+ from pptx import Presentation
305
+ from pptx.util import Inches
306
+ import uuid
307
 
308
  try:
309
+ # Create presentation directly
310
  prs = Presentation()
311
  presentation_id = str(uuid.uuid4())
312
  presentations[presentation_id] = prs
313
+ set_current_presentation_id(presentation_id)
314
 
315
+ return presentation_id, "✅ Test presentation created successfully"
 
 
 
 
316
  except Exception as e:
317
+ return None, f"Error creating presentation: {str(e)}"
318
+ def add_test_slide(presentation_id, title="Test Slide", content="Test content"):
319
+ """Add a test slide"""
 
 
320
  try:
321
+ if presentation_id not in presentations:
322
+ return False, "Presentation not found"
323
+
324
+ prs = presentations[presentation_id]
325
+
326
+ # Add slide with title and content layout
327
+ slide_layout = prs.slide_layouts[1] # Title and content layout
328
  slide = prs.slides.add_slide(slide_layout)
329
 
330
+ # Set title
331
  if slide.shapes.title:
332
  slide.shapes.title.text = title
333
 
334
+ # Set content
335
+ if len(slide.placeholders) > 1:
336
  slide.placeholders[1].text = content
337
 
338
+ return True, f"Slide added: {title}"
 
 
339
  except Exception as e:
 
340
  return False, f"Error adding slide: {str(e)}"
341
+ def save_test_presentation(presentation_id):
342
+ """Save test presentation to file"""
 
343
  try:
344
+ if presentation_id not in presentations:
345
+ return None, "Presentation not found"
 
 
 
 
 
346
 
347
+ prs = presentations[presentation_id]
348
+ temp_file = tempfile.NamedTemporaryFile(suffix='.pptx', delete=False)
349
+ prs.save(temp_file.name)
350
 
351
+ return temp_file.name, "✅ Presentation saved successfully"
 
 
 
 
 
 
352
  except Exception as e:
353
+ return None, f"Error saving presentation: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
354
  # Gradio Interface Functions
355
+ def create_presentation_from_json(slides_data):
356
+ """Create PowerPoint from JSON data (for Gradio testing)"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  try:
358
+ if isinstance(slides_data, str):
359
+ data = json.loads(slides_data)
360
  else:
361
+ data = slides_data
362
 
363
  # Create presentation
364
+ presentation_id, create_message = create_test_presentation()
 
 
365
  if not presentation_id:
366
+ return None, create_message
367
 
368
  # Add slides
 
369
  slide_count = 0
370
+ for slide_info in data.get("slides", []):
371
+ success, message = add_test_slide(
372
+ presentation_id,
373
+ slide_info.get("title", f"Slide {slide_count + 1}"),
374
+ slide_info.get("content", "Sample content")
375
+ )
376
  if success:
377
  slide_count += 1
378
 
 
380
  return None, "❌ No slides were created successfully"
381
 
382
  # Save presentation
383
+ file_path, save_message = save_test_presentation(presentation_id)
 
384
 
385
+ if file_path:
386
+ return file_path, f"✅ Presentation created with {slide_count} slides"
387
  else:
388
+ return None, save_message
389
 
 
 
390
  except Exception as e:
391
+ logger.error(f"Error creating presentation: {str(e)}")
392
  return None, f"❌ Error: {str(e)}"
 
393
  def get_server_status():
394
+ """Get current server status for display"""
 
 
 
395
  return f"""
396
  🟢 **Server Status**: Running
397
+ 🔧 **Transport**: Streamable HTTP
398
+ 📦 **Tools Available**: 32+ (All modules registered)
399
+ 🎯 **MCP Version**: 2024-11-05
400
+ ⚡ **Server Version**: 2.1.0
401
  📊 **Presentations Loaded**: {len(presentations)}
402
+ 🎮 **Current Presentation**: {current_presentation_id or 'None'}
403
+ """
404
+ # Create Gradio Interface
 
 
405
  def create_gradio_interface():
406
+ """Create the main Gradio interface"""
407
 
408
+ with gr.Blocks(title="PowerPoint MCP Server - Streamable HTTP", theme=gr.themes.Soft()) as demo:
409
  gr.Markdown("# 🎯 PowerPoint MCP Server")
410
+ gr.Markdown("**Transport**: Streamable HTTP | **Endpoint**: `/mcp` | **Tools**: 32+ available")
411
 
412
+ with gr.Tab("🚀 Test Interface"):
413
  with gr.Row():
414
  with gr.Column():
415
+ gr.Markdown("### Create Presentation from JSON")
416
+ slides_input = gr.Textbox(
417
+ label="Slides Data (JSON)",
418
+ value='{"slides": [{"title": "Welcome", "content": "This is slide 1"}, {"title": "About Us", "content": "This is slide 2"}]}',
419
+ lines=12,
420
+ placeholder='{"slides": [{"title": "Slide Title", "content": "Slide content"}]}'
421
  )
422
+ create_btn = gr.Button("🚀 Create Presentation", variant="primary", size="lg")
 
423
 
424
  with gr.Column():
425
+ status_output = gr.Textbox(
426
+ label="Creation Status",
427
+ interactive=False,
428
+ lines=3
 
429
  )
430
+ download_btn = gr.DownloadButton(
431
+ label="📥 Download PowerPoint",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  visible=False,
433
  size="lg"
434
  )
435
 
436
  with gr.Tab("🔧 MCP Configuration"):
437
+ gr.Markdown("## MCP Server Settings for Your Chatbot")
438
 
439
  with gr.Row():
440
  with gr.Column():
441
+ gr.Markdown("### Configuration Details")
442
+ config_code = f"""**Name**: PowerPoint Creator
443
+ **Description**: AI-powered PowerPoint presentation generator with 32 professional tools and streamable HTTP transport
444
+ **URL**: https://brucewayne1-auxoppt.hf.space/mcp
445
+ **Transport**: Stream"""
446
+ gr.Code(config_code, language="yaml")
 
 
 
 
 
 
 
447
 
448
  with gr.Column():
449
+ gr.Markdown("### Quick Copy")
450
+ gr.Textbox(
451
+ value="https://brucewayne1-auxoppt.hf.space/mcp",
452
+ label="MCP Server URL",
453
+ interactive=True
454
+ )
455
+ gr.Textbox(
456
+ value="Stream",
457
+ label="Transport Type",
458
+ interactive=False
459
+ )
 
 
 
 
 
 
 
 
 
 
460
 
461
  with gr.Tab("📊 Server Status"):
462
+ status_text = gr.Textbox(
463
  value=get_server_status(),
464
  label="Server Information",
465
+ lines=10,
466
  interactive=False
467
  )
468
+
469
+ refresh_btn = gr.Button("🔄 Refresh Status", variant="secondary")
470
+
471
+ refresh_btn.click(
472
+ fn=get_server_status,
473
+ outputs=[status_text]
474
+ )
475
+
476
+ with gr.Tab("📖 Available Tools"):
477
+ gr.Markdown("## All Available MCP Tools")
478
+
479
+ tools_info = """
480
+ ### 🎯 Presentation Management (7 tools)
481
+ • create_presentation • create_presentation_from_template • open_presentation
482
+ • save_presentation • get_presentation_info • get_template_file_info • set_core_properties
483
+ ### 📝 Content Management (6 tools)
484
+ • add_slide • get_slide_info • extract_slide_text • extract_presentation_text
485
+ • populate_placeholder • add_bullet_points • manage_text • manage_image
486
+ ### 🎨 Template Operations (7 tools)
487
+ • list_slide_templates • apply_slide_template • create_slide_from_template
488
+ • create_presentation_from_templates • get_template_info • auto_generate_presentation • optimize_slide_text
489
+ ### 🏗️ Structural Elements (4 tools)
490
+ • add_table • format_table_cell • add_shape • add_chart
491
+ ### ✨ Professional Design (3 tools)
492
+ • apply_professional_design • apply_picture_effects • manage_fonts
493
+ ### 🔧 Specialized Features (5 tools)
494
+ • manage_hyperlinks • manage_slide_masters • add_connector • update_chart_data • manage_slide_transitions
495
+ """
496
+ gr.Markdown(tools_info)
497
 
498
  # Event handlers
499
  create_btn.click(
500
+ fn=create_presentation_from_json,
501
+ inputs=[slides_input],
502
+ outputs=[download_btn, status_output]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
  ).then(
504
  fn=lambda x: gr.update(visible=bool(x)),
505
+ inputs=[download_btn],
506
+ outputs=[download_btn]
507
  )
508
 
509
  return demo
510
+ # MCP Server startup function
511
+ def start_mcp_server():
512
+ """Start the MCP server with streamable HTTP transport"""
513
+ try:
514
+ logger.info("Starting MCP Server with streamable HTTP transport...")
515
+ # Use streamable HTTP transport on port 8000
516
+ mcp_app.run(transport='streamable-http', port=8000)
517
+ except Exception as e:
518
+ logger.error(f"Failed to start MCP server: {str(e)}")
 
 
 
 
 
 
519
  # Main execution
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
520
  if __name__ == "__main__":
521
+ # Start MCP server in background thread
522
+ mcp_thread = Thread(target=start_mcp_server, daemon=True)
523
+ mcp_thread.start()
524
+
525
+ # Wait for MCP server to start
526
+ time.sleep(3)
527
+ logger.info("MCP Server started on port 8000 with streamable HTTP transport")
528
+
529
+ # Launch Gradio interface
530
+ demo = create_gradio_interface()
531
+ demo.launch(
532
+ server_name="0.0.0.0",
533
+ server_port=7860,
534
+ share=True,
535
+ show_error=True,
536
+ show_api=False
537
+ )