@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,206 @@
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 mammoth = require("mammoth");
9
+ const { Document, Packer, Paragraph, TextRun } = require("docx");
10
+ const ReadWordSchema = z.object({
11
+ path: z.string().describe("Path to the Word file (.docx)"),
12
+ });
13
+ const WriteWordSchema = z.object({
14
+ path: z.string().describe("Path to the Word file (.docx)"),
15
+ title: z.string().optional().describe("Document title"),
16
+ content: z.string().describe("Text content to write to the document"),
17
+ });
18
+ const GetActiveOfficeSchema = z.object({
19
+ filename: z.string().optional().describe("Name of the specific file to get data from. If omitted, the active document is used."),
20
+ });
21
+ const WriteActiveWordSchema = z.object({
22
+ filename: z.string().optional().describe("Name of the document. If omitted, the active document is used."),
23
+ operation: z.enum(["append", "replace_text", "replace_all"]).describe("How to edit the active Word document"),
24
+ text: z.string().describe("Text to insert or use as replacement content"),
25
+ findText: z.string().optional().describe("Text to find when operation is replace_text"),
26
+ replaceAllMatches: z.boolean().optional().default(false).describe("Replace every match instead of only the first match"),
27
+ });
28
+ export const wordTools = {
29
+ tools: [
30
+ {
31
+ name: "read_word",
32
+ description: "Read text content from a Word file (.docx)",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ path: { type: "string", description: "Path to the Word file" },
37
+ },
38
+ required: ["path"],
39
+ },
40
+ },
41
+ {
42
+ name: "write_word",
43
+ description: "Create a new Word file with text content",
44
+ inputSchema: {
45
+ type: "object",
46
+ properties: {
47
+ path: { type: "string", description: "Path to the Word file" },
48
+ title: { type: "string", description: "Document title (optional)" },
49
+ content: { type: "string", description: "Text content" },
50
+ },
51
+ required: ["path", "content"],
52
+ },
53
+ },
54
+ {
55
+ name: "list_active_word",
56
+ description: "List all open Word documents",
57
+ inputSchema: {
58
+ type: "object",
59
+ properties: {},
60
+ },
61
+ },
62
+ {
63
+ name: "get_active_word",
64
+ description: "Get text content from a currently open (active) Word document without saving",
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ filename: { type: "string", description: "Name of the document (e.g., 'Document1.docx'). If omitted, the active document is used." },
69
+ },
70
+ },
71
+ },
72
+ {
73
+ name: "write_active_word",
74
+ description: "Edit a currently open Word document without saving. Supports append, replace_text, and replace_all.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ filename: { type: "string", description: "Name of the document (e.g., 'Document1.docx'). If omitted, the active document is used." },
79
+ operation: {
80
+ type: "string",
81
+ enum: ["append", "replace_text", "replace_all"],
82
+ description: "append adds text to the end, replace_text replaces matching text, replace_all replaces the whole document body",
83
+ },
84
+ text: { type: "string", description: "Text to insert or use as replacement content" },
85
+ findText: { type: "string", description: "Text to find when operation is replace_text" },
86
+ replaceAllMatches: { type: "boolean", description: "Replace every match instead of only the first match" },
87
+ },
88
+ required: ["operation", "text"],
89
+ },
90
+ },
91
+ ],
92
+ handlers: {
93
+ async read_word(args) {
94
+ const { path: filePath } = ReadWordSchema.parse(args);
95
+ const absolutePath = path.resolve(filePath);
96
+ if (!fs.existsSync(absolutePath)) {
97
+ throw new McpError(ErrorCode.InvalidParams, `File not found: ${filePath}`);
98
+ }
99
+ const result = await mammoth.extractRawText({ path: absolutePath });
100
+ return textContent(result.value);
101
+ },
102
+ async write_word(args) {
103
+ const { path: filePath, title, content } = WriteWordSchema.parse(args);
104
+ const absolutePath = path.resolve(filePath);
105
+ const doc = new Document({
106
+ sections: [{
107
+ properties: {},
108
+ children: [
109
+ ...(title ? [new Paragraph({
110
+ children: [new TextRun({
111
+ text: title,
112
+ bold: true,
113
+ size: 32,
114
+ font: "Malgun Gothic",
115
+ })],
116
+ spacing: { after: 400 },
117
+ })] : []),
118
+ ...content.split("\n").map(line => new Paragraph({
119
+ children: [new TextRun({
120
+ text: line,
121
+ font: "Malgun Gothic",
122
+ })],
123
+ })),
124
+ ],
125
+ }],
126
+ });
127
+ const buffer = await Packer.toBuffer(doc);
128
+ fs.writeFileSync(absolutePath, buffer);
129
+ return textContent(`Successfully created Word file at ${filePath}`);
130
+ },
131
+ async list_active_word() {
132
+ const script = `
133
+ $word = [Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application')
134
+ $names = @($word.Documents | ForEach-Object { $_.Name })
135
+ ConvertTo-Json -InputObject @($names) -Compress
136
+ `;
137
+ return textContent(runPowerShell(script));
138
+ },
139
+ async get_active_word(args) {
140
+ const { filename } = GetActiveOfficeSchema.parse(args);
141
+ const script = `
142
+ $word = [Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application')
143
+ $target = "${filename || ""}"
144
+ $doc = if ($target) {
145
+ $word.Documents | Where-Object { $_.Name -eq $target -or $_.FullName -eq $target } | Select-Object -First 1
146
+ } else {
147
+ $word.ActiveDocument
148
+ }
149
+ if ($null -eq $doc) { throw "Document '$target' not found or no active document." }
150
+ $doc.Content.Text
151
+ `;
152
+ return textContent(runPowerShell(script));
153
+ },
154
+ async write_active_word(args) {
155
+ const { filename, operation, text, findText, replaceAllMatches } = WriteActiveWordSchema.parse(args);
156
+ if (operation === "replace_text" && !findText) {
157
+ throw new McpError(ErrorCode.InvalidParams, "findText is required when operation is replace_text.");
158
+ }
159
+ const filenameBase64 = toBase64(filename || "");
160
+ const textBase64 = toBase64(text);
161
+ const findTextBase64 = toBase64(findText || "");
162
+ const script = `
163
+ function Decode-Utf8([string]$value) {
164
+ [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($value))
165
+ }
166
+
167
+ $word = [Runtime.InteropServices.Marshal]::GetActiveObject('Word.Application')
168
+ $target = Decode-Utf8 '${filenameBase64}'
169
+ $operation = '${operation}'
170
+ $text = Decode-Utf8 '${textBase64}'
171
+ $findText = Decode-Utf8 '${findTextBase64}'
172
+ $replaceAllMatches = [System.Convert]::ToBoolean('${replaceAllMatches}')
173
+
174
+ $doc = if ($target) {
175
+ $word.Documents | Where-Object { $_.Name -eq $target -or $_.FullName -eq $target } | Select-Object -First 1
176
+ } else {
177
+ $word.ActiveDocument
178
+ }
179
+ if ($null -eq $doc) { throw "Document '$target' not found or no active document." }
180
+
181
+ if ($operation -eq 'append') {
182
+ $range = $doc.Content
183
+ $range.Collapse(0)
184
+ $range.InsertAfter([Environment]::NewLine + $text)
185
+ $result = @{ Document = $doc.Name; Operation = $operation; Changed = 1 }
186
+ } elseif ($operation -eq 'replace_all') {
187
+ $doc.Content.Text = $text
188
+ $result = @{ Document = $doc.Name; Operation = $operation; Changed = 1 }
189
+ } elseif ($operation -eq 'replace_text') {
190
+ $range = $doc.Content
191
+ $find = $range.Find
192
+ $find.ClearFormatting()
193
+ $find.Replacement.ClearFormatting()
194
+ $replaceMode = if ($replaceAllMatches) { 2 } else { 1 }
195
+ $changed = $find.Execute($findText, $false, $false, $false, $false, $false, $true, 1, $false, $text, $replaceMode)
196
+ $result = @{ Document = $doc.Name; Operation = $operation; Changed = [int][bool]$changed; ReplacedAllMatches = $replaceAllMatches }
197
+ } else {
198
+ throw "Unsupported Word operation: $operation"
199
+ }
200
+
201
+ ConvertTo-Json -InputObject $result -Compress
202
+ `;
203
+ return textContent(runPowerShell(script));
204
+ },
205
+ },
206
+ };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@developer.k/ms-office-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for local Microsoft Office files and active desktop Office documents",
5
+ "type": "module",
6
+ "main": "build/index.js",
7
+ "bin": {
8
+ "ms-office-mcp": "build/index.js"
9
+ },
10
+ "files": [
11
+ "build",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "prepack": "npm run build",
18
+ "start": "node build/index.js",
19
+ "dev": "ts-node --esm src/index.ts"
20
+ },
21
+ "keywords": [
22
+ "mcp",
23
+ "model-context-protocol",
24
+ "office",
25
+ "excel",
26
+ "word",
27
+ "powerpoint"
28
+ ],
29
+ "author": "",
30
+ "license": "ISC",
31
+ "engines": {
32
+ "node": ">=20"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.29.0",
36
+ "docx": "^9.7.1",
37
+ "mammoth": "^1.12.0",
38
+ "officeparser": "^7.2.1",
39
+ "pptxgenjs": "^4.0.1",
40
+ "xlsx": "^0.18.5",
41
+ "zod": "^4.4.3"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^25.9.3",
45
+ "ts-node": "^10.9.2",
46
+ "typescript": "^6.0.3"
47
+ }
48
+ }