@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.
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,265 @@
1
+ # MS Office MCP
2
+
3
+ `@developer.k/ms-office-mcp` is an MCP server for working with Microsoft Office documents from an MCP client.
4
+
5
+ It supports two workflows:
6
+
7
+ - File-based operations for `.xlsx`, `.docx`, and `.pptx` files.
8
+ - Active desktop Office automation for documents already open in Excel, Word, or PowerPoint on Windows.
9
+
10
+ The server communicates over stdio and is designed to be launched by MCP clients such as Claude Desktop, Cursor, or other MCP-compatible tools.
11
+
12
+ ## Requirements
13
+
14
+ - Node.js 20 or later.
15
+ - Microsoft Office desktop apps for active-document tools.
16
+ - Windows PowerShell and Office COM automation for tools that read or edit currently open Office documents.
17
+
18
+ File-based tools can run anywhere Node.js can access the target files. Active Excel, Word, and PowerPoint tools require a Windows desktop session with the corresponding Office app already running.
19
+
20
+ ## Usage with MCP Clients
21
+
22
+ ### Using npx
23
+
24
+ Add this server to your MCP client configuration:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "ms-office-mcp": {
30
+ "command": "npx",
31
+ "args": ["-y", "-p", "@developer.k/ms-office-mcp", "ms-office-mcp"]
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ No environment variables are required.
38
+
39
+ ### Local Development
40
+
41
+ ```bash
42
+ npm install
43
+ npm run build
44
+ npm start
45
+ ```
46
+
47
+ For TypeScript development:
48
+
49
+ ```bash
50
+ npm run dev
51
+ ```
52
+
53
+ ## Tools
54
+
55
+ ### File-Based Tools
56
+
57
+ | Tool | Office app | Purpose | Key inputs |
58
+ | --- | --- | --- | --- |
59
+ | `list_sheets` | Excel | List worksheet names in an `.xlsx` file. | `path` |
60
+ | `read_excel` | Excel | Read rows from a sheet as JSON objects. | `path`, optional `sheetName` |
61
+ | `write_excel` | Excel | Create or overwrite a sheet from object rows. | `path`, `sheetName`, `data` |
62
+ | `read_word` | Word | Extract raw text from a `.docx` file. | `path` |
63
+ | `write_word` | Word | Create a `.docx` file with optional title and text content. | `path`, optional `title`, `content` |
64
+ | `read_pptx` | PowerPoint | Extract text from a `.pptx` file. | `path` |
65
+ | `write_pptx` | PowerPoint | Create a `.pptx` file from slide title/content pairs. | `path`, `slides` |
66
+
67
+ ## Active Office Tools
68
+
69
+ Active Office tools connect to an already-running Office desktop application. They operate on the open document but do not automatically save it.
70
+
71
+ Use the `filename` argument when multiple documents are open. If omitted, the active document is used.
72
+
73
+ | Tool | Office app | Purpose | Key inputs |
74
+ | --- | --- | --- | --- |
75
+ | `list_active_excel` | Excel | List open Excel workbooks. | none |
76
+ | `get_active_excel` | Excel | Read the active sheet from an open workbook. | optional `filename` |
77
+ | `write_active_excel` | Excel | Write object rows into the active sheet of an open workbook. | optional `filename`, `data` |
78
+ | `list_active_word` | Word | List open Word documents. | none |
79
+ | `get_active_word` | Word | Read text from an open Word document. | optional `filename` |
80
+ | `write_active_word` | Word | Edit an open Word document. | optional `filename`, `operation`, `text`, optional `findText`, optional `replaceAllMatches` |
81
+ | `list_active_pptx` | PowerPoint | List open PowerPoint presentations. | none |
82
+ | `get_active_pptx` | PowerPoint | Read slide text from an open presentation. | optional `filename` |
83
+ | `write_active_pptx` | PowerPoint | Edit slide-level text in an open presentation. | optional `filename`, `operation`, optional `slideIndex`, optional `title`, `text`, optional `findText`, optional `replaceAllMatches` |
84
+ | `inspect_active_pptx` | PowerPoint | Inspect slides and shapes, including IDs, names, types, positions, sizes, text, and basic colors. | optional `filename`, optional `slideIndex` |
85
+ | `edit_active_pptx_object` | PowerPoint | Edit a specific shape without selecting it in the UI. | optional `filename`, `slideIndex`, `shapeId` or `shapeName`, `operation` |
86
+
87
+ ### Supported Operations
88
+
89
+ | Tool | Operations |
90
+ | --- | --- |
91
+ | `write_active_word` | `append`, `replace_text`, `replace_all` |
92
+ | `write_active_pptx` | `add_slide`, `replace_text`, `replace_slide_text` |
93
+ | `edit_active_pptx_object` | `set_text`, `set_position`, `set_size`, `set_fill_color`, `set_line_color`, `delete` |
94
+
95
+ ### Common Input Examples
96
+
97
+ Read a specific Excel sheet:
98
+
99
+ ```json
100
+ {
101
+ "path": "C:\\Documents\\report.xlsx",
102
+ "sheetName": "Sheet1"
103
+ }
104
+ ```
105
+
106
+ Replace text in an open Word document:
107
+
108
+ ```json
109
+ {
110
+ "filename": "Document1.docx",
111
+ "operation": "replace_text",
112
+ "findText": "old phrase",
113
+ "text": "new phrase",
114
+ "replaceAllMatches": true
115
+ }
116
+ ```
117
+
118
+ Edit a PowerPoint shape:
119
+
120
+ ```json
121
+ {
122
+ "filename": "Presentation1.pptx",
123
+ "slideIndex": 2,
124
+ "shapeId": 5,
125
+ "operation": "set_fill_color",
126
+ "color": "#FFAA00"
127
+ }
128
+ ```
129
+
130
+ ## Example Workflow
131
+
132
+ 1. Ask the MCP client to inspect slide 3 with `inspect_active_pptx`.
133
+ 2. Pick the returned shape ID for the object you want to modify.
134
+ 3. Call `edit_active_pptx_object` with that shape ID.
135
+ 4. Review the open PowerPoint window.
136
+ 5. Save manually if the result is correct.
137
+
138
+ ## Prompt Examples
139
+
140
+ These are examples of prompts you can type into an MCP client after this server is configured.
141
+
142
+ ### Excel
143
+
144
+ ```text
145
+ Read the Excel file at C:\Documents\sales.xlsx and summarize the first sheet.
146
+ ```
147
+
148
+ ```text
149
+ List all sheets in C:\Documents\sales.xlsx, then read the sheet named "Q2".
150
+ ```
151
+
152
+ ```text
153
+ Create an Excel file at C:\Documents\tasks.xlsx with columns Task, Owner, Status, and DueDate. Add five sample rows.
154
+ ```
155
+
156
+ ```text
157
+ I have an Excel workbook open. Read the active sheet and tell me which rows have missing Status values.
158
+ ```
159
+
160
+ ```text
161
+ Write these rows into the active Excel workbook: Task=Review proposal, Owner=Kim, Status=Done; Task=Send deck, Owner=Lee, Status=In progress.
162
+ ```
163
+
164
+ ### Word
165
+
166
+ ```text
167
+ Read C:\Documents\proposal.docx and give me a concise summary with action items.
168
+ ```
169
+
170
+ ```text
171
+ Create a Word document at C:\Documents\meeting-notes.docx titled "Meeting Notes" with sections for Attendees, Decisions, and Next Steps.
172
+ ```
173
+
174
+ ```text
175
+ I have a Word document open. Replace every occurrence of "FY25" with "FY26", but do not save the file.
176
+ ```
177
+
178
+ ```text
179
+ Append this paragraph to the open Word document: "Please review the updated timeline before Friday."
180
+ ```
181
+
182
+ ```text
183
+ Read the currently active Word document and rewrite the executive summary in a more concise tone.
184
+ ```
185
+
186
+ ### PowerPoint
187
+
188
+ ```text
189
+ Read C:\Documents\quarterly-review.pptx and summarize each slide in one sentence.
190
+ ```
191
+
192
+ ```text
193
+ Create a PowerPoint file at C:\Documents\project-update.pptx with slides for Overview, Timeline, Risks, and Next Steps.
194
+ ```
195
+
196
+ ```text
197
+ I have a PowerPoint deck open. Replace "Q1" with "Q2" on slide 2 only, without saving the file.
198
+ ```
199
+
200
+ ```text
201
+ Inspect slide 3 of the open PowerPoint deck and show me the shapes with their IDs, names, text, position, and size.
202
+ ```
203
+
204
+ ```text
205
+ On slide 3 of the open PowerPoint deck, find the shape that contains "Revenue" and change its fill color to #FFAA00.
206
+ ```
207
+
208
+ ```text
209
+ Add a new slide to the open PowerPoint deck titled "Next Steps" with three bullets: finalize scope, assign owners, confirm timeline.
210
+ ```
211
+
212
+ ```text
213
+ On slide 5, move the shape named "Title 1" to the top-left corner and make it wider.
214
+ ```
215
+
216
+ ### Cross-Document Tasks
217
+
218
+ ```text
219
+ Read the active Excel workbook, summarize the key numbers, and add a new slide to the open PowerPoint deck with the summary.
220
+ ```
221
+
222
+ ```text
223
+ Read C:\Documents\notes.docx and create a PowerPoint deck at C:\Documents\summary.pptx with one slide per major section.
224
+ ```
225
+
226
+ ```text
227
+ Inspect the open PowerPoint deck, find slides that mention "risk", and append a risk summary paragraph to the open Word document.
228
+ ```
229
+
230
+ ## Docker
231
+
232
+ The Docker image can be used for file-based operations where the target files are mounted into the container.
233
+
234
+ ```bash
235
+ docker compose up --build
236
+ ```
237
+
238
+ Active desktop Office automation is not available inside the provided Linux container because it depends on Windows Office COM automation.
239
+
240
+ ## Publishing
241
+
242
+ ```bash
243
+ npm run build
244
+ npm pack --dry-run
245
+ npm publish --access public
246
+ ```
247
+
248
+ The package includes only:
249
+
250
+ - `build`
251
+ - `README.md`
252
+ - `LICENSE`
253
+ - `package.json`
254
+
255
+ ## Limitations
256
+
257
+ - Active-document tools are Windows-only.
258
+ - Active-document tools require the corresponding Office app to be running.
259
+ - Active-document tools modify the open document but do not save automatically.
260
+ - `.doc` and `.ppt` legacy formats are not the primary target; use `.docx` and `.pptx`.
261
+ - PowerPoint object editing currently targets shapes by slide and shape identifier/name.
262
+
263
+ ## Security Notes
264
+
265
+ This package does not require API keys or credentials. It can read and modify files or active Office documents that the server process can access, so configure it only in MCP clients and environments you trust.
package/build/index.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { z } from "zod";
6
+ import { excelTools } from "./tools/excel.js";
7
+ import { pptTools } from "./tools/ppt.js";
8
+ import { wordTools } from "./tools/word.js";
9
+ class OfficeMcpServer {
10
+ server;
11
+ tools;
12
+ handlers;
13
+ constructor() {
14
+ this.server = new Server({
15
+ name: "ms-office-mcp",
16
+ version: "1.0.0",
17
+ }, {
18
+ capabilities: {
19
+ tools: {},
20
+ },
21
+ });
22
+ const modules = [excelTools, wordTools, pptTools];
23
+ this.tools = modules.flatMap((module) => module.tools);
24
+ this.handlers = this.mergeHandlers(modules);
25
+ this.setupTools();
26
+ }
27
+ mergeHandlers(modules) {
28
+ return modules.reduce((handlers, module) => {
29
+ for (const [name, handler] of Object.entries(module.handlers)) {
30
+ handlers[name] = handler;
31
+ }
32
+ return handlers;
33
+ }, {});
34
+ }
35
+ setupTools() {
36
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
37
+ tools: this.tools,
38
+ }));
39
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
40
+ const { name, arguments: args } = request.params;
41
+ try {
42
+ const handler = this.handlers[name];
43
+ if (!handler) {
44
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
45
+ }
46
+ return await handler(args);
47
+ }
48
+ catch (error) {
49
+ if (error instanceof z.ZodError) {
50
+ throw new McpError(ErrorCode.InvalidParams, `Invalid arguments: ${error.issues.map((e) => e.message).join(", ")}`);
51
+ }
52
+ throw error;
53
+ }
54
+ });
55
+ }
56
+ async run() {
57
+ const transport = new StdioServerTransport();
58
+ await this.server.connect(transport);
59
+ console.error("MS Office MCP server running on stdio");
60
+ }
61
+ }
62
+ const server = new OfficeMcpServer();
63
+ server.run().catch(console.error);
@@ -0,0 +1,36 @@
1
+ import { execSync } from "node:child_process";
2
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
3
+ export function textContent(text) {
4
+ return {
5
+ content: [{ type: "text", text }],
6
+ };
7
+ }
8
+ export function toBase64(value) {
9
+ return Buffer.from(value, "utf8").toString("base64");
10
+ }
11
+ export function runPowerShell(script) {
12
+ try {
13
+ const fullScript = `
14
+ $OutputEncoding = [Console]::InputEncoding = [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
15
+ try {
16
+ ${script}
17
+ } catch {
18
+ Write-Error $_.Exception.Message
19
+ exit 1
20
+ }
21
+ `;
22
+ const buffer = Buffer.from(fullScript, "utf16le");
23
+ const base64 = buffer.toString("base64");
24
+ return execSync(`powershell -NoProfile -NonInteractive -EncodedCommand ${base64}`, {
25
+ encoding: "utf8",
26
+ windowsHide: true,
27
+ }).trim();
28
+ }
29
+ catch (error) {
30
+ const errorMessage = error.stderr || error.message;
31
+ if (errorMessage.includes("0x800401E3")) {
32
+ throw new McpError(ErrorCode.InternalError, "Application is not running or no active document found.");
33
+ }
34
+ throw new McpError(ErrorCode.InternalError, `PowerShell error: ${errorMessage}`);
35
+ }
36
+ }
@@ -0,0 +1,230 @@
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 } from "../office-utils.js";
7
+ const require = createRequire(import.meta.url);
8
+ const XLSX = require("xlsx");
9
+ const ListSheetsSchema = z.object({
10
+ path: z.string().describe("Path to the Excel file"),
11
+ });
12
+ const ReadExcelSchema = z.object({
13
+ path: z.string().describe("Path to the Excel file"),
14
+ sheetName: z.string().optional().describe("Name of the sheet to read. Defaults to the first sheet."),
15
+ });
16
+ const WriteExcelSchema = z.object({
17
+ path: z.string().describe("Path to the Excel file"),
18
+ sheetName: z.string().describe("Name of the sheet to write to"),
19
+ data: z.array(z.any()).describe("Array of objects representing the data to write"),
20
+ });
21
+ const GetActiveOfficeSchema = z.object({
22
+ filename: z.string().optional().describe("Name of the specific file to get data from. If omitted, the active document is used."),
23
+ });
24
+ const WriteActiveExcelSchema = z.object({
25
+ filename: z.string().optional().describe("Name of the workbook. If omitted, the active workbook is used."),
26
+ data: z.array(z.any()).describe("Array of objects representing the data to write"),
27
+ });
28
+ export const excelTools = {
29
+ tools: [
30
+ {
31
+ name: "list_sheets",
32
+ description: "List all sheet names in an Excel file",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ path: { type: "string", description: "Path to the Excel file" },
37
+ },
38
+ required: ["path"],
39
+ },
40
+ },
41
+ {
42
+ name: "read_excel",
43
+ description: "Read data from an Excel sheet",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ path: { type: "string", description: "Path to the Excel file" },
48
+ sheetName: { type: "string", description: "Sheet name (optional)" },
49
+ },
50
+ required: ["path"],
51
+ },
52
+ },
53
+ {
54
+ name: "write_excel",
55
+ description: "Write data to an Excel sheet (creates new or overwrites)",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ path: { type: "string", description: "Path to the Excel file" },
60
+ sheetName: { type: "string", description: "Sheet name" },
61
+ data: {
62
+ type: "array",
63
+ items: { type: "object" },
64
+ description: "Data as an array of objects",
65
+ },
66
+ },
67
+ required: ["path", "sheetName", "data"],
68
+ },
69
+ },
70
+ {
71
+ name: "list_active_excel",
72
+ description: "List all open Excel workbooks",
73
+ inputSchema: {
74
+ type: "object",
75
+ properties: {},
76
+ },
77
+ },
78
+ {
79
+ name: "get_active_excel",
80
+ description: "Get data from a currently open (active) Excel workbook without saving",
81
+ inputSchema: {
82
+ type: "object",
83
+ properties: {
84
+ filename: { type: "string", description: "Name of the workbook (e.g., 'Book1.xlsx'). If omitted, the active workbook is used." },
85
+ },
86
+ },
87
+ },
88
+ {
89
+ name: "write_active_excel",
90
+ description: "Write data to a currently open (active) Excel workbook without saving",
91
+ inputSchema: {
92
+ type: "object",
93
+ properties: {
94
+ filename: { type: "string", description: "Name of the workbook (e.g., 'Book1.xlsx'). If omitted, the active workbook is used." },
95
+ data: {
96
+ type: "array",
97
+ items: { type: "object" },
98
+ description: "Data as an array of objects",
99
+ },
100
+ },
101
+ required: ["data"],
102
+ },
103
+ },
104
+ ],
105
+ handlers: {
106
+ async list_sheets(args) {
107
+ const { path: filePath } = ListSheetsSchema.parse(args);
108
+ const absolutePath = path.resolve(filePath);
109
+ if (!fs.existsSync(absolutePath)) {
110
+ throw new McpError(ErrorCode.InvalidParams, `File not found: ${filePath}`);
111
+ }
112
+ const workbook = XLSX.readFile(absolutePath);
113
+ return textContent(JSON.stringify(workbook.SheetNames, null, 2));
114
+ },
115
+ async read_excel(args) {
116
+ const { path: filePath, sheetName } = ReadExcelSchema.parse(args);
117
+ const absolutePath = path.resolve(filePath);
118
+ if (!fs.existsSync(absolutePath)) {
119
+ throw new McpError(ErrorCode.InvalidParams, `File not found: ${filePath}`);
120
+ }
121
+ const workbook = XLSX.readFile(absolutePath);
122
+ const targetSheet = sheetName || workbook.SheetNames[0];
123
+ if (!workbook.Sheets[targetSheet]) {
124
+ throw new McpError(ErrorCode.InvalidParams, `Sheet not found: ${targetSheet}`);
125
+ }
126
+ const data = XLSX.utils.sheet_to_json(workbook.Sheets[targetSheet]);
127
+ return textContent(JSON.stringify(data, null, 2));
128
+ },
129
+ async write_excel(args) {
130
+ const { path: filePath, sheetName, data } = WriteExcelSchema.parse(args);
131
+ const absolutePath = path.resolve(filePath);
132
+ let workbook;
133
+ if (fs.existsSync(absolutePath)) {
134
+ workbook = XLSX.readFile(absolutePath);
135
+ }
136
+ else {
137
+ workbook = XLSX.utils.book_new();
138
+ }
139
+ const worksheet = XLSX.utils.json_to_sheet(data);
140
+ if (workbook.Sheets[sheetName]) {
141
+ workbook.Sheets[sheetName] = worksheet;
142
+ }
143
+ else {
144
+ XLSX.utils.book_append_sheet(workbook, worksheet, sheetName);
145
+ }
146
+ XLSX.writeFile(workbook, absolutePath);
147
+ return textContent(`Successfully wrote to ${filePath} (Sheet: ${sheetName})`);
148
+ },
149
+ async list_active_excel() {
150
+ const script = `
151
+ $excel = [Runtime.InteropServices.Marshal]::GetActiveObject('Excel.Application')
152
+ $names = @($excel.Workbooks | ForEach-Object { $_.Name })
153
+ ConvertTo-Json -InputObject @($names) -Compress
154
+ `;
155
+ return textContent(runPowerShell(script));
156
+ },
157
+ async get_active_excel(args) {
158
+ const { filename } = GetActiveOfficeSchema.parse(args);
159
+ const script = `
160
+ $excel = [Runtime.InteropServices.Marshal]::GetActiveObject('Excel.Application')
161
+ $target = "${filename || ""}"
162
+ $workbook = if ($target) {
163
+ $excel.Workbooks | Where-Object { $_.Name -eq $target -or $_.FullName -eq $target } | Select-Object -First 1
164
+ } else {
165
+ $excel.ActiveWorkbook
166
+ }
167
+ if ($null -eq $workbook) { throw "Workbook '$target' not found or no active workbook." }
168
+ $sheet = $workbook.ActiveSheet
169
+ $range = $sheet.UsedRange
170
+ $values = $range.Value2
171
+ if ($null -eq $values) { return "[]" }
172
+ if ($values.GetType().IsArray) {
173
+ $rows = $values.GetLength(0)
174
+ $cols = $values.GetLength(1)
175
+ $header = @()
176
+ for ($c = 1; $c -le $cols; $c++) { $header += $values[1, $c] }
177
+ $data = @()
178
+ for ($r = 2; $r -le $rows; $r++) {
179
+ $obj = @{}
180
+ for ($c = 1; $c -le $cols; $c++) {
181
+ $key = $header[$c-1]
182
+ if ($null -ne $key) { $obj[$key.ToString()] = $values[$r, $c] }
183
+ }
184
+ $data += $obj
185
+ }
186
+ ConvertTo-Json -InputObject @($data) -Compress
187
+ } else {
188
+ ConvertTo-Json -InputObject @($values) -Compress
189
+ }
190
+ `;
191
+ return textContent(runPowerShell(script));
192
+ },
193
+ async write_active_excel(args) {
194
+ const { filename, data } = WriteActiveExcelSchema.parse(args);
195
+ const jsonBase64 = Buffer.from(JSON.stringify(data)).toString("base64");
196
+ const script = `
197
+ $excel = [Runtime.InteropServices.Marshal]::GetActiveObject('Excel.Application')
198
+ $target = "${filename || ""}"
199
+ $workbook = if ($target) {
200
+ $excel.Workbooks | Where-Object { $_.Name -eq $target -or $_.FullName -eq $target } | Select-Object -First 1
201
+ } else {
202
+ $excel.ActiveWorkbook
203
+ }
204
+ if ($null -eq $workbook) { throw "Workbook '$target' not found or no active workbook." }
205
+ $sheet = $workbook.ActiveSheet
206
+
207
+ $json = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${jsonBase64}'))
208
+ $data = $json | ConvertFrom-Json
209
+ if ($data -is [PSCustomObject]) { $data = @($data) }
210
+
211
+ if ($data.Count -gt 0) {
212
+ $headers = $data[0].PSObject.Properties.Name
213
+ for ($c = 0; $c -lt $headers.Count; $c++) {
214
+ $sheet.Cells.Item(1, $c + 1).Value2 = $headers[$c]
215
+ }
216
+ for ($r = 0; $r -lt $data.Count; $r++) {
217
+ for ($c = 0; $c -lt $headers.Count; $c++) {
218
+ $val = $data[$r].$($headers[$c])
219
+ $sheet.Cells.Item($r + 2, $c + 1).Value2 = $val
220
+ }
221
+ }
222
+ "Successfully wrote $($data.Count) rows to $($workbook.Name)"
223
+ } else {
224
+ "No data to write"
225
+ }
226
+ `;
227
+ return textContent(runPowerShell(script));
228
+ },
229
+ },
230
+ };