@developer.k/ms-office-mcp 1.0.0

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.
@@ -0,0 +1,521 @@
1
+ import { createRequire } from "node:module";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
5
+ import { z } from "zod";
6
+ import { runPowerShell, textContent, toBase64 } from "../office-utils.js";
7
+ const require = createRequire(import.meta.url);
8
+ const PptxGenJS = require("pptxgenjs");
9
+ const officeParser = require("officeparser");
10
+ const ReadPptSchema = z.object({
11
+ path: z.string().describe("Path to the PowerPoint file (.pptx)"),
12
+ });
13
+ const WritePptSchema = z.object({
14
+ path: z.string().describe("Path to the PowerPoint file (.pptx)"),
15
+ slides: z.array(z.object({
16
+ title: z.string().optional().describe("Slide title"),
17
+ content: z.string().describe("Slide text content"),
18
+ })).describe("Array of slides with title and content"),
19
+ });
20
+ const GetActiveOfficeSchema = z.object({
21
+ filename: z.string().optional().describe("Name of the specific file to get data from. If omitted, the active document is used."),
22
+ });
23
+ const WriteActivePptSchema = z.object({
24
+ filename: z.string().optional().describe("Name of the presentation. If omitted, the active presentation is used."),
25
+ operation: z.enum(["add_slide", "replace_text", "replace_slide_text"]).describe("How to edit the active PowerPoint presentation"),
26
+ slideIndex: z.number().int().positive().optional().describe("1-based slide index for slide-scoped operations"),
27
+ title: z.string().optional().describe("Slide title when adding a slide"),
28
+ text: z.string().describe("Text to insert or use as replacement content"),
29
+ findText: z.string().optional().describe("Text to find when operation is replace_text"),
30
+ replaceAllMatches: z.boolean().optional().default(false).describe("Replace every match instead of only the first match"),
31
+ });
32
+ const InspectActivePptSchema = z.object({
33
+ filename: z.string().optional().describe("Name of the presentation. If omitted, the active presentation is used."),
34
+ slideIndex: z.number().int().positive().optional().describe("1-based slide index to inspect. If omitted, all slides are inspected."),
35
+ });
36
+ const EditActivePptObjectSchema = z.object({
37
+ filename: z.string().optional().describe("Name of the presentation. If omitted, the active presentation is used."),
38
+ slideIndex: z.number().int().positive().describe("1-based slide index containing the target shape"),
39
+ shapeId: z.number().int().positive().optional().describe("PowerPoint shape ID from inspect_active_pptx"),
40
+ shapeName: z.string().optional().describe("PowerPoint shape name from inspect_active_pptx"),
41
+ operation: z.enum(["set_text", "set_position", "set_size", "set_fill_color", "set_line_color", "delete"]).describe("Object edit operation"),
42
+ text: z.string().optional().describe("Text to set when operation is set_text"),
43
+ left: z.number().optional().describe("Left position in points when operation is set_position"),
44
+ top: z.number().optional().describe("Top position in points when operation is set_position"),
45
+ width: z.number().positive().optional().describe("Width in points when operation is set_size"),
46
+ height: z.number().positive().optional().describe("Height in points when operation is set_size"),
47
+ color: z.string().optional().describe("RGB hex color such as #FFAA00 or FFAA00 for color operations"),
48
+ });
49
+ export const pptTools = {
50
+ tools: [
51
+ {
52
+ name: "read_pptx",
53
+ description: "Read text content from a PowerPoint file (.pptx)",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: {
57
+ path: { type: "string", description: "Path to the PowerPoint file" },
58
+ },
59
+ required: ["path"],
60
+ },
61
+ },
62
+ {
63
+ name: "write_pptx",
64
+ description: "Create a new PowerPoint file with slides",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ path: { type: "string", description: "Path to the PowerPoint file" },
69
+ slides: {
70
+ type: "array",
71
+ items: {
72
+ type: "object",
73
+ properties: {
74
+ title: { type: "string", description: "Slide title (optional)" },
75
+ content: { type: "string", description: "Slide content" },
76
+ },
77
+ required: ["content"],
78
+ },
79
+ description: "Array of slides",
80
+ },
81
+ },
82
+ required: ["path", "slides"],
83
+ },
84
+ },
85
+ {
86
+ name: "list_active_pptx",
87
+ description: "List all open PowerPoint presentations",
88
+ inputSchema: {
89
+ type: "object",
90
+ properties: {},
91
+ },
92
+ },
93
+ {
94
+ name: "get_active_pptx",
95
+ description: "Get slide text from a currently open (active) PowerPoint presentation without saving",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ filename: { type: "string", description: "Name of the presentation (e.g., 'Presentation1.pptx'). If omitted, the active presentation is used." },
100
+ },
101
+ },
102
+ },
103
+ {
104
+ name: "write_active_pptx",
105
+ description: "Edit a currently open PowerPoint presentation without saving. Supports add_slide, replace_text, and replace_slide_text.",
106
+ inputSchema: {
107
+ type: "object",
108
+ properties: {
109
+ filename: { type: "string", description: "Name of the presentation (e.g., 'Presentation1.pptx'). If omitted, the active presentation is used." },
110
+ operation: {
111
+ type: "string",
112
+ enum: ["add_slide", "replace_text", "replace_slide_text"],
113
+ description: "add_slide creates a new slide, replace_text replaces matching text in shapes, replace_slide_text rewrites one slide's text boxes",
114
+ },
115
+ slideIndex: { type: "number", description: "1-based slide index for slide-scoped operations" },
116
+ title: { type: "string", description: "Slide title when adding a slide" },
117
+ text: { type: "string", description: "Text to insert or use as replacement content" },
118
+ findText: { type: "string", description: "Text to find when operation is replace_text" },
119
+ replaceAllMatches: { type: "boolean", description: "Replace every match instead of only the first match" },
120
+ },
121
+ required: ["operation", "text"],
122
+ },
123
+ },
124
+ {
125
+ name: "inspect_active_pptx",
126
+ description: "Inspect slides and shapes in a currently open PowerPoint presentation. Returns shape IDs, names, types, positions, sizes, text, and basic styling.",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ filename: { type: "string", description: "Name of the presentation (e.g., 'Presentation1.pptx'). If omitted, the active presentation is used." },
131
+ slideIndex: { type: "number", description: "1-based slide index to inspect. If omitted, all slides are inspected." },
132
+ },
133
+ },
134
+ },
135
+ {
136
+ name: "edit_active_pptx_object",
137
+ description: "Edit a non-selected PowerPoint shape by slideIndex and shapeId or shapeName. Supports text, position, size, fill color, line color, and delete.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ filename: { type: "string", description: "Name of the presentation (e.g., 'Presentation1.pptx'). If omitted, the active presentation is used." },
142
+ slideIndex: { type: "number", description: "1-based slide index containing the target shape" },
143
+ shapeId: { type: "number", description: "Shape ID from inspect_active_pptx" },
144
+ shapeName: { type: "string", description: "Shape name from inspect_active_pptx" },
145
+ operation: {
146
+ type: "string",
147
+ enum: ["set_text", "set_position", "set_size", "set_fill_color", "set_line_color", "delete"],
148
+ description: "Object edit operation",
149
+ },
150
+ text: { type: "string", description: "Text to set when operation is set_text" },
151
+ left: { type: "number", description: "Left position in points when operation is set_position" },
152
+ top: { type: "number", description: "Top position in points when operation is set_position" },
153
+ width: { type: "number", description: "Width in points when operation is set_size" },
154
+ height: { type: "number", description: "Height in points when operation is set_size" },
155
+ color: { type: "string", description: "RGB hex color such as #FFAA00 or FFAA00 for color operations" },
156
+ },
157
+ required: ["slideIndex", "operation"],
158
+ },
159
+ },
160
+ ],
161
+ handlers: {
162
+ async read_pptx(args) {
163
+ const { path: filePath } = ReadPptSchema.parse(args);
164
+ const absolutePath = path.resolve(filePath);
165
+ if (!fs.existsSync(absolutePath)) {
166
+ throw new McpError(ErrorCode.InvalidParams, `File not found: ${filePath}`);
167
+ }
168
+ const result = await new Promise((resolve, reject) => {
169
+ officeParser.parseOffice(absolutePath, (data, err) => {
170
+ if (err)
171
+ reject(err);
172
+ else
173
+ resolve(data);
174
+ });
175
+ });
176
+ const text = typeof result === "string" ? result : (result.toText ? result.toText() : JSON.stringify(result));
177
+ return textContent(text);
178
+ },
179
+ async write_pptx(args) {
180
+ const { path: filePath, slides } = WritePptSchema.parse(args);
181
+ const absolutePath = path.resolve(filePath);
182
+ const pres = new PptxGenJS();
183
+ slides.forEach(slideData => {
184
+ const slide = pres.addSlide();
185
+ if (slideData.title) {
186
+ slide.addText(slideData.title, {
187
+ x: 0.5, y: 0.5, fontSize: 24, bold: true,
188
+ fontFace: "Malgun Gothic",
189
+ });
190
+ }
191
+ slide.addText(slideData.content, {
192
+ x: 0.5, y: 1.5, fontSize: 18,
193
+ fontFace: "Malgun Gothic",
194
+ });
195
+ });
196
+ await pres.writeFile({ fileName: absolutePath });
197
+ return textContent(`Successfully created PowerPoint file at ${filePath}`);
198
+ },
199
+ async list_active_pptx() {
200
+ const script = `
201
+ $ppt = [Runtime.InteropServices.Marshal]::GetActiveObject('PowerPoint.Application')
202
+ $names = @($ppt.Presentations | ForEach-Object { $_.Name })
203
+ ConvertTo-Json -InputObject @($names) -Compress
204
+ `;
205
+ return textContent(runPowerShell(script));
206
+ },
207
+ async get_active_pptx(args) {
208
+ const { filename } = GetActiveOfficeSchema.parse(args);
209
+ const script = `
210
+ $ppt = [Runtime.InteropServices.Marshal]::GetActiveObject('PowerPoint.Application')
211
+ $target = "${filename || ""}"
212
+ $pres = if ($target) {
213
+ $ppt.Presentations | Where-Object { $_.Name -eq $target -or $_.FullName -eq $target } | Select-Object -First 1
214
+ } else {
215
+ $ppt.ActivePresentation
216
+ }
217
+ if ($null -eq $pres) { throw "Presentation '$target' not found or no active presentation." }
218
+ $slides = foreach ($slide in $pres.Slides) {
219
+ $text = ""
220
+ foreach ($shape in $slide.Shapes) {
221
+ if ($shape.HasTextFrame -and $shape.TextFrame.HasText) {
222
+ $text += $shape.TextFrame.TextRange.Text + "\`n"
223
+ }
224
+ }
225
+ @{ Index = $slide.SlideIndex; Content = $text.Trim() }
226
+ }
227
+ ConvertTo-Json -InputObject @($slides) -Compress
228
+ `;
229
+ return textContent(runPowerShell(script));
230
+ },
231
+ async write_active_pptx(args) {
232
+ const { filename, operation, slideIndex, title, text, findText, replaceAllMatches } = WriteActivePptSchema.parse(args);
233
+ if (operation === "replace_text" && !findText) {
234
+ throw new McpError(ErrorCode.InvalidParams, "findText is required when operation is replace_text.");
235
+ }
236
+ if (operation === "replace_slide_text" && !slideIndex) {
237
+ throw new McpError(ErrorCode.InvalidParams, "slideIndex is required when operation is replace_slide_text.");
238
+ }
239
+ const filenameBase64 = toBase64(filename || "");
240
+ const titleBase64 = toBase64(title || "");
241
+ const textBase64 = toBase64(text);
242
+ const findTextBase64 = toBase64(findText || "");
243
+ const script = `
244
+ function Decode-Utf8([string]$value) {
245
+ [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($value))
246
+ }
247
+
248
+ function Replace-FirstLiteral([string]$source, [string]$find, [string]$replacement) {
249
+ $index = $source.IndexOf($find, [System.StringComparison]::Ordinal)
250
+ if ($index -lt 0) { return @{ Text = $source; Changed = $false } }
251
+ $before = $source.Substring(0, $index)
252
+ $after = $source.Substring($index + $find.Length)
253
+ return @{ Text = $before + $replacement + $after; Changed = $true }
254
+ }
255
+
256
+ $ppt = [Runtime.InteropServices.Marshal]::GetActiveObject('PowerPoint.Application')
257
+ $target = Decode-Utf8 '${filenameBase64}'
258
+ $operation = '${operation}'
259
+ $slideIndex = ${slideIndex || 0}
260
+ $title = Decode-Utf8 '${titleBase64}'
261
+ $text = Decode-Utf8 '${textBase64}'
262
+ $findText = Decode-Utf8 '${findTextBase64}'
263
+ $replaceAllMatches = [System.Convert]::ToBoolean('${replaceAllMatches}')
264
+
265
+ $pres = if ($target) {
266
+ $ppt.Presentations | Where-Object { $_.Name -eq $target -or $_.FullName -eq $target } | Select-Object -First 1
267
+ } else {
268
+ $ppt.ActivePresentation
269
+ }
270
+ if ($null -eq $pres) { throw "Presentation '$target' not found or no active presentation." }
271
+
272
+ if ($operation -eq 'add_slide') {
273
+ $slide = $pres.Slides.Add($pres.Slides.Count + 1, 12)
274
+ if ($title) {
275
+ $titleBox = $slide.Shapes.AddTextbox(1, 36, 36, 648, 54)
276
+ $titleBox.TextFrame.TextRange.Text = $title
277
+ $titleBox.TextFrame.TextRange.Font.Size = 28
278
+ $titleBox.TextFrame.TextRange.Font.Bold = $true
279
+ }
280
+ $bodyBox = $slide.Shapes.AddTextbox(1, 36, 108, 648, 360)
281
+ $bodyBox.TextFrame.TextRange.Text = $text
282
+ $bodyBox.TextFrame.TextRange.Font.Size = 18
283
+ $result = @{ Presentation = $pres.Name; Operation = $operation; SlideIndex = $slide.SlideIndex; Changed = 1 }
284
+ } elseif ($operation -eq 'replace_slide_text') {
285
+ if ($slideIndex -lt 1 -or $slideIndex -gt $pres.Slides.Count) { throw "Slide index $slideIndex is out of range." }
286
+ $slide = $pres.Slides.Item($slideIndex)
287
+ $changed = 0
288
+ $textShapes = @()
289
+ foreach ($shape in $slide.Shapes) {
290
+ if ($shape.HasTextFrame -and $shape.TextFrame.HasText) {
291
+ $textShapes += $shape
292
+ }
293
+ }
294
+ if ($textShapes.Count -eq 0) {
295
+ $bodyBox = $slide.Shapes.AddTextbox(1, 36, 108, 648, 360)
296
+ $bodyBox.TextFrame.TextRange.Text = $text
297
+ $changed = 1
298
+ } else {
299
+ $textShapes[0].TextFrame.TextRange.Text = $text
300
+ for ($i = 1; $i -lt $textShapes.Count; $i++) {
301
+ $textShapes[$i].TextFrame.TextRange.Text = ""
302
+ }
303
+ $changed = 1
304
+ }
305
+ $result = @{ Presentation = $pres.Name; Operation = $operation; SlideIndex = $slideIndex; Changed = $changed }
306
+ } elseif ($operation -eq 'replace_text') {
307
+ $changed = 0
308
+ $slides = if ($slideIndex -gt 0) {
309
+ if ($slideIndex -gt $pres.Slides.Count) { throw "Slide index $slideIndex is out of range." }
310
+ @($pres.Slides.Item($slideIndex))
311
+ } else {
312
+ @($pres.Slides | ForEach-Object { $_ })
313
+ }
314
+
315
+ foreach ($slide in $slides) {
316
+ foreach ($shape in $slide.Shapes) {
317
+ if ($shape.HasTextFrame -and $shape.TextFrame.HasText) {
318
+ $current = [string]$shape.TextFrame.TextRange.Text
319
+ if ($replaceAllMatches) {
320
+ $next = $current.Replace($findText, $text)
321
+ if ($next -ne $current) {
322
+ $shape.TextFrame.TextRange.Text = $next
323
+ $changed++
324
+ }
325
+ } else {
326
+ $replacement = Replace-FirstLiteral $current $findText $text
327
+ if ($replacement.Changed) {
328
+ $shape.TextFrame.TextRange.Text = $replacement.Text
329
+ $changed++
330
+ break
331
+ }
332
+ }
333
+ }
334
+ }
335
+ if (-not $replaceAllMatches -and $changed -gt 0) { break }
336
+ }
337
+ $result = @{ Presentation = $pres.Name; Operation = $operation; SlideIndex = $slideIndex; Changed = $changed; ReplacedAllMatches = $replaceAllMatches }
338
+ } else {
339
+ throw "Unsupported PowerPoint operation: $operation"
340
+ }
341
+
342
+ ConvertTo-Json -InputObject $result -Compress
343
+ `;
344
+ return textContent(runPowerShell(script));
345
+ },
346
+ async inspect_active_pptx(args) {
347
+ const { filename, slideIndex } = InspectActivePptSchema.parse(args);
348
+ const filenameBase64 = toBase64(filename || "");
349
+ const script = `
350
+ function Decode-Utf8([string]$value) {
351
+ [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($value))
352
+ }
353
+
354
+ function Get-RgbHex($rgb) {
355
+ if ($null -eq $rgb -or $rgb -lt 0) { return $null }
356
+ $red = $rgb -band 255
357
+ $green = ($rgb -shr 8) -band 255
358
+ $blue = ($rgb -shr 16) -band 255
359
+ return ('#{0:X2}{1:X2}{2:X2}' -f $red, $green, $blue)
360
+ }
361
+
362
+ $ppt = [Runtime.InteropServices.Marshal]::GetActiveObject('PowerPoint.Application')
363
+ $target = Decode-Utf8 '${filenameBase64}'
364
+ $slideIndex = ${slideIndex || 0}
365
+ $pres = if ($target) {
366
+ $ppt.Presentations | Where-Object { $_.Name -eq $target -or $_.FullName -eq $target } | Select-Object -First 1
367
+ } else {
368
+ $ppt.ActivePresentation
369
+ }
370
+ if ($null -eq $pres) { throw "Presentation '$target' not found or no active presentation." }
371
+
372
+ $slides = if ($slideIndex -gt 0) {
373
+ if ($slideIndex -gt $pres.Slides.Count) { throw "Slide index $slideIndex is out of range." }
374
+ @($pres.Slides.Item($slideIndex))
375
+ } else {
376
+ @($pres.Slides | ForEach-Object { $_ })
377
+ }
378
+
379
+ $resultSlides = foreach ($slide in $slides) {
380
+ $shapes = foreach ($shape in $slide.Shapes) {
381
+ $text = $null
382
+ if ($shape.HasTextFrame -and $shape.TextFrame.HasText) {
383
+ $text = [string]$shape.TextFrame.TextRange.Text
384
+ }
385
+
386
+ $fillColor = $null
387
+ $lineColor = $null
388
+ try {
389
+ if ($shape.Fill.Visible) { $fillColor = Get-RgbHex $shape.Fill.ForeColor.RGB }
390
+ } catch {}
391
+ try {
392
+ if ($shape.Line.Visible) { $lineColor = Get-RgbHex $shape.Line.ForeColor.RGB }
393
+ } catch {}
394
+
395
+ @{
396
+ Id = $shape.Id
397
+ Name = $shape.Name
398
+ Type = $shape.Type
399
+ AutoShapeType = $shape.AutoShapeType
400
+ Left = [math]::Round($shape.Left, 2)
401
+ Top = [math]::Round($shape.Top, 2)
402
+ Width = [math]::Round($shape.Width, 2)
403
+ Height = [math]::Round($shape.Height, 2)
404
+ HasText = [bool]($shape.HasTextFrame -and $shape.TextFrame.HasText)
405
+ Text = $text
406
+ FillColor = $fillColor
407
+ LineColor = $lineColor
408
+ }
409
+ }
410
+
411
+ @{
412
+ Index = $slide.SlideIndex
413
+ Name = $slide.Name
414
+ Width = [math]::Round($pres.PageSetup.SlideWidth, 2)
415
+ Height = [math]::Round($pres.PageSetup.SlideHeight, 2)
416
+ Shapes = @($shapes)
417
+ }
418
+ }
419
+
420
+ ConvertTo-Json -InputObject @{ Presentation = $pres.Name; Slides = @($resultSlides) } -Depth 6 -Compress
421
+ `;
422
+ return textContent(runPowerShell(script));
423
+ },
424
+ async edit_active_pptx_object(args) {
425
+ const { filename, slideIndex, shapeId, shapeName, operation, text, left, top, width, height, color } = EditActivePptObjectSchema.parse(args);
426
+ if (!shapeId && !shapeName) {
427
+ throw new McpError(ErrorCode.InvalidParams, "shapeId or shapeName is required.");
428
+ }
429
+ if (operation === "set_text" && text === undefined) {
430
+ throw new McpError(ErrorCode.InvalidParams, "text is required when operation is set_text.");
431
+ }
432
+ if (operation === "set_position" && (left === undefined || top === undefined)) {
433
+ throw new McpError(ErrorCode.InvalidParams, "left and top are required when operation is set_position.");
434
+ }
435
+ if (operation === "set_size" && (width === undefined || height === undefined)) {
436
+ throw new McpError(ErrorCode.InvalidParams, "width and height are required when operation is set_size.");
437
+ }
438
+ if ((operation === "set_fill_color" || operation === "set_line_color") && !color) {
439
+ throw new McpError(ErrorCode.InvalidParams, "color is required for color operations.");
440
+ }
441
+ const normalizedColor = color ? color.replace(/^#/, "") : "";
442
+ if (normalizedColor && !/^[0-9a-fA-F]{6}$/.test(normalizedColor)) {
443
+ throw new McpError(ErrorCode.InvalidParams, "color must be a 6-digit RGB hex value, such as #FFAA00.");
444
+ }
445
+ const filenameBase64 = toBase64(filename || "");
446
+ const shapeNameBase64 = toBase64(shapeName || "");
447
+ const textBase64 = toBase64(text || "");
448
+ const script = `
449
+ function Decode-Utf8([string]$value) {
450
+ [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($value))
451
+ }
452
+
453
+ function Convert-HexToOfficeRgb([string]$hex) {
454
+ if (-not $hex) { return 0 }
455
+ $red = [Convert]::ToInt32($hex.Substring(0, 2), 16)
456
+ $green = [Convert]::ToInt32($hex.Substring(2, 2), 16)
457
+ $blue = [Convert]::ToInt32($hex.Substring(4, 2), 16)
458
+ return $red + ($green -shl 8) + ($blue -shl 16)
459
+ }
460
+
461
+ $ppt = [Runtime.InteropServices.Marshal]::GetActiveObject('PowerPoint.Application')
462
+ $target = Decode-Utf8 '${filenameBase64}'
463
+ $slideIndex = ${slideIndex}
464
+ $shapeId = ${shapeId || 0}
465
+ $shapeName = Decode-Utf8 '${shapeNameBase64}'
466
+ $operation = '${operation}'
467
+ $text = Decode-Utf8 '${textBase64}'
468
+ $left = ${left ?? 0}
469
+ $top = ${top ?? 0}
470
+ $width = ${width ?? 0}
471
+ $height = ${height ?? 0}
472
+ $color = '${normalizedColor}'
473
+
474
+ $pres = if ($target) {
475
+ $ppt.Presentations | Where-Object { $_.Name -eq $target -or $_.FullName -eq $target } | Select-Object -First 1
476
+ } else {
477
+ $ppt.ActivePresentation
478
+ }
479
+ if ($null -eq $pres) { throw "Presentation '$target' not found or no active presentation." }
480
+ if ($slideIndex -gt $pres.Slides.Count) { throw "Slide index $slideIndex is out of range." }
481
+
482
+ $slide = $pres.Slides.Item($slideIndex)
483
+ $targetShape = $null
484
+ foreach ($shape in $slide.Shapes) {
485
+ if (($shapeId -gt 0 -and $shape.Id -eq $shapeId) -or ($shapeName -and $shape.Name -eq $shapeName)) {
486
+ $targetShape = $shape
487
+ break
488
+ }
489
+ }
490
+ if ($null -eq $targetShape) { throw "Shape not found on slide $slideIndex." }
491
+
492
+ $shapeInfo = @{ Id = $targetShape.Id; Name = $targetShape.Name; SlideIndex = $slideIndex }
493
+
494
+ if ($operation -eq 'set_text') {
495
+ if (-not $targetShape.HasTextFrame) { throw "Target shape does not support text." }
496
+ $targetShape.TextFrame.TextRange.Text = $text
497
+ } elseif ($operation -eq 'set_position') {
498
+ $targetShape.Left = $left
499
+ $targetShape.Top = $top
500
+ } elseif ($operation -eq 'set_size') {
501
+ $targetShape.Width = $width
502
+ $targetShape.Height = $height
503
+ } elseif ($operation -eq 'set_fill_color') {
504
+ $targetShape.Fill.Visible = $true
505
+ $targetShape.Fill.ForeColor.RGB = Convert-HexToOfficeRgb $color
506
+ } elseif ($operation -eq 'set_line_color') {
507
+ $targetShape.Line.Visible = $true
508
+ $targetShape.Line.ForeColor.RGB = Convert-HexToOfficeRgb $color
509
+ } elseif ($operation -eq 'delete') {
510
+ $targetShape.Delete()
511
+ } else {
512
+ throw "Unsupported object operation: $operation"
513
+ }
514
+
515
+ $result = @{ Presentation = $pres.Name; Operation = $operation; Changed = 1; Shape = $shapeInfo }
516
+ ConvertTo-Json -InputObject $result -Depth 4 -Compress
517
+ `;
518
+ return textContent(runPowerShell(script));
519
+ },
520
+ },
521
+ };