@tiwater/office-mcp 0.2.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/_shared/mcp-stdio.mjs +152 -0
- package/_shared/tool-runtime.mjs +108 -0
- package/office/README.md +28 -0
- package/office/index.mjs +515 -0
- package/package.json +26 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
|
|
3
|
+
const JSONRPC_VERSION = '2.0';
|
|
4
|
+
const SUPPORTED_PROTOCOL_VERSIONS = ['2025-06-18', '2025-03-26', '2024-11-05', '2024-10-07'];
|
|
5
|
+
const FALLBACK_PROTOCOL_VERSION = '2025-06-18';
|
|
6
|
+
|
|
7
|
+
function writeMessage(message) {
|
|
8
|
+
process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toError(code, message, data) {
|
|
12
|
+
return { code, message, ...(data === undefined ? {} : { data }) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeToolCallError(error) {
|
|
16
|
+
if (!error) return toError(-32603, 'Unknown error');
|
|
17
|
+
if (error.code && error.message) return error;
|
|
18
|
+
return toError(-32603, error instanceof Error ? error.message : String(error));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class McpStdioServer {
|
|
22
|
+
constructor({ name, version, instructions, tools, callTool, logger = console.error }) {
|
|
23
|
+
this.serverInfo = { name, version };
|
|
24
|
+
this.instructions = instructions;
|
|
25
|
+
this.tools = tools;
|
|
26
|
+
this.callTool = callTool;
|
|
27
|
+
this.logger = logger;
|
|
28
|
+
this.lineBuffer = '';
|
|
29
|
+
this.binaryBuffer = Buffer.alloc(0);
|
|
30
|
+
this.initialized = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
start() {
|
|
34
|
+
process.stdin.on('data', chunk => this.#onData(chunk));
|
|
35
|
+
process.stdin.on('end', () => process.exit(0));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#onData(chunk) {
|
|
39
|
+
const text = chunk.toString('utf8');
|
|
40
|
+
|
|
41
|
+
if (this.binaryBuffer.length > 0 || text.includes('Content-Length:')) {
|
|
42
|
+
this.binaryBuffer = Buffer.concat([this.binaryBuffer, chunk]);
|
|
43
|
+
this.#drainContentLengthBuffer();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.lineBuffer += text;
|
|
48
|
+
while (true) {
|
|
49
|
+
const newlineIndex = this.lineBuffer.indexOf('\n');
|
|
50
|
+
if (newlineIndex === -1) return;
|
|
51
|
+
const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, '').trim();
|
|
52
|
+
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
53
|
+
if (!line) continue;
|
|
54
|
+
this.#parseAndHandle(line, null);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#drainContentLengthBuffer() {
|
|
59
|
+
while (true) {
|
|
60
|
+
const headerEnd = this.binaryBuffer.indexOf('\r\n\r\n');
|
|
61
|
+
if (headerEnd === -1) return;
|
|
62
|
+
|
|
63
|
+
const headerText = this.binaryBuffer.subarray(0, headerEnd).toString('utf8');
|
|
64
|
+
const lengthMatch = headerText.match(/Content-Length:\s*(\d+)/i);
|
|
65
|
+
if (!lengthMatch) {
|
|
66
|
+
this.logger('Missing Content-Length header');
|
|
67
|
+
this.binaryBuffer = Buffer.alloc(0);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const contentLength = Number(lengthMatch[1]);
|
|
72
|
+
const messageStart = headerEnd + 4;
|
|
73
|
+
const messageEnd = messageStart + contentLength;
|
|
74
|
+
if (this.binaryBuffer.length < messageEnd) return;
|
|
75
|
+
|
|
76
|
+
const body = this.binaryBuffer.subarray(messageStart, messageEnd).toString('utf8');
|
|
77
|
+
this.binaryBuffer = this.binaryBuffer.subarray(messageEnd);
|
|
78
|
+
this.#parseAndHandle(body, null);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#parseAndHandle(body, idHint) {
|
|
83
|
+
let message;
|
|
84
|
+
try {
|
|
85
|
+
message = JSON.parse(body);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
writeMessage({ jsonrpc: JSONRPC_VERSION, id: idHint, error: toError(-32700, 'Parse error', String(error)) });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
void this.#handleMessage(message);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async #handleMessage(message) {
|
|
95
|
+
if (!message || message.jsonrpc !== JSONRPC_VERSION || typeof message.method !== 'string') {
|
|
96
|
+
if ('id' in (message || {})) {
|
|
97
|
+
writeMessage({ jsonrpc: JSONRPC_VERSION, id: message.id ?? null, error: toError(-32600, 'Invalid Request') });
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { id, method, params = {} } = message;
|
|
103
|
+
const isNotification = id === undefined;
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
switch (method) {
|
|
107
|
+
case 'initialize': {
|
|
108
|
+
const requested = params.protocolVersion;
|
|
109
|
+
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : FALLBACK_PROTOCOL_VERSION;
|
|
110
|
+
const result = {
|
|
111
|
+
protocolVersion,
|
|
112
|
+
capabilities: { tools: {} },
|
|
113
|
+
serverInfo: this.serverInfo,
|
|
114
|
+
...(this.instructions ? { instructions: this.instructions } : {}),
|
|
115
|
+
};
|
|
116
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
case 'notifications/initialized': {
|
|
120
|
+
this.initialized = true;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
case 'ping': {
|
|
124
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result: {} });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
case 'tools/list': {
|
|
128
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result: { tools: this.tools } });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
case 'tools/call': {
|
|
132
|
+
const name = params?.name;
|
|
133
|
+
const args = params?.arguments ?? {};
|
|
134
|
+
if (typeof name !== 'string' || !name) {
|
|
135
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, error: toError(-32602, 'Invalid params: missing tool name') });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const result = await this.callTool(name, args);
|
|
139
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
default: {
|
|
143
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, error: toError(-32601, `Method not found: ${method}`) });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (!isNotification) {
|
|
148
|
+
writeMessage({ jsonrpc: JSONRPC_VERSION, id, error: normalizeToolCallError(error) });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
|
|
7
|
+
const sharedDir = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
export const repoRoot = path.resolve(sharedDir, '..', '..');
|
|
9
|
+
|
|
10
|
+
export function createToolResult(payload) {
|
|
11
|
+
return {
|
|
12
|
+
structuredContent: payload,
|
|
13
|
+
content: [
|
|
14
|
+
{
|
|
15
|
+
type: 'text',
|
|
16
|
+
text: JSON.stringify(payload, null, 2),
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function resolveRepoPath(...segments) {
|
|
23
|
+
return path.join(repoRoot, ...segments);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function commandCandidate(command, argsPrefix = [], options = {}) {
|
|
27
|
+
return { command, argsPrefix, ...options };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runCandidateChain(candidates, args, options = {}) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
for (const candidate of candidates) {
|
|
33
|
+
try {
|
|
34
|
+
return await runCommand(candidate, args, options);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error?.code === 'ENOENT') {
|
|
37
|
+
errors.push(`${candidate.command}: not found`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`No runnable command candidate succeeded. ${errors.join('; ')}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function runJsonCandidateChain(candidates, args, options = {}) {
|
|
47
|
+
const result = await runCandidateChain(candidates, args, options);
|
|
48
|
+
const text = result.stdout.trim();
|
|
49
|
+
if (!text) return { ...result, json: null };
|
|
50
|
+
try {
|
|
51
|
+
return { ...result, json: JSON.parse(text) };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error(`Expected JSON output but received: ${text.slice(0, 300)}${text.length > 300 ? '…' : ''}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function withTempJsonFile(data, fn) {
|
|
58
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'tiwater-mcp-'));
|
|
59
|
+
const filePath = path.join(dir, 'payload.json');
|
|
60
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf8');
|
|
61
|
+
try {
|
|
62
|
+
return await fn(filePath);
|
|
63
|
+
} finally {
|
|
64
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function maybeReadJson(filePath) {
|
|
69
|
+
const text = await fs.readFile(filePath, 'utf8');
|
|
70
|
+
return JSON.parse(text);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function requireString(value, label) {
|
|
74
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
75
|
+
throw Object.assign(new Error(`${label} must be a non-empty string`), { code: -32602 });
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function runCommand(candidate, args, options) {
|
|
81
|
+
const env = { ...process.env, ...(candidate.env || {}), ...(options.env || {}) };
|
|
82
|
+
const cwd = candidate.cwd || options.cwd || repoRoot;
|
|
83
|
+
const commandArgs = [...(candidate.argsPrefix || []), ...args];
|
|
84
|
+
|
|
85
|
+
return await new Promise((resolve, reject) => {
|
|
86
|
+
const child = spawn(candidate.command, commandArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
87
|
+
let stdout = '';
|
|
88
|
+
let stderr = '';
|
|
89
|
+
|
|
90
|
+
child.stdout.on('data', chunk => {
|
|
91
|
+
stdout += chunk.toString();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
child.stderr.on('data', chunk => {
|
|
95
|
+
stderr += chunk.toString();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
child.on('error', reject);
|
|
99
|
+
child.on('close', code => {
|
|
100
|
+
const allowedExitCodes = options.allowedExitCodes ?? [0];
|
|
101
|
+
if (allowedExitCodes.includes(code)) {
|
|
102
|
+
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
reject(new Error(`${candidate.command} ${commandArgs.join(' ')} failed with exit code ${code}\n${stderr || stdout}`));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}
|
package/office/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# office MCP server
|
|
2
|
+
|
|
3
|
+
Shared stdio MCP server for Office document workflows.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
- `docx_inspect`
|
|
8
|
+
- `docx_list_migration_choices`
|
|
9
|
+
- `docx_migrate_template`
|
|
10
|
+
- `docx_verify_migration`
|
|
11
|
+
- `docx_compare`
|
|
12
|
+
- `docx_validate_template_transform`
|
|
13
|
+
- `docx_export_json`
|
|
14
|
+
- `xlsx_inspect`
|
|
15
|
+
- `xlsx_export_json`
|
|
16
|
+
- `xlsx_validate`
|
|
17
|
+
- `pptx_inspect`
|
|
18
|
+
- `pptx_inspect_detail`
|
|
19
|
+
- `pptx_export_json`
|
|
20
|
+
|
|
21
|
+
## Run
|
|
22
|
+
|
|
23
|
+
Install `@tiwater/office-mcp` together with the runtime versions required by
|
|
24
|
+
the consumer, then run `tiwater-office-mcp` as a stdio MCP server.
|
|
25
|
+
|
|
26
|
+
The server invokes published `tiwater-docx`, `tiwater-xlsx`, and
|
|
27
|
+
`tiwater-pptx` commands from `PATH`. It does not require a source checkout or
|
|
28
|
+
fall back to local projects.
|
package/office/index.mjs
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { McpStdioServer } from '../_shared/mcp-stdio.mjs';
|
|
5
|
+
import {
|
|
6
|
+
commandCandidate,
|
|
7
|
+
createToolResult,
|
|
8
|
+
maybeReadJson,
|
|
9
|
+
requireString,
|
|
10
|
+
resolveRepoPath,
|
|
11
|
+
runCandidateChain,
|
|
12
|
+
runJsonCandidateChain,
|
|
13
|
+
withTempJsonFile,
|
|
14
|
+
} from '../_shared/tool-runtime.mjs';
|
|
15
|
+
|
|
16
|
+
const docxCandidates = [
|
|
17
|
+
commandCandidate('tiwater-docx'),
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const xlsxCandidates = [
|
|
21
|
+
commandCandidate('tiwater-xlsx'),
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const pptxCandidates = [
|
|
25
|
+
commandCandidate('tiwater-pptx'),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function templateMigrationInputSchema() {
|
|
29
|
+
return {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
source: { type: 'string', description: 'Path to the current source DOCX.' },
|
|
33
|
+
baseline: { type: 'string', description: 'Path to the selected current baseline DOCX.' },
|
|
34
|
+
output: { type: 'string', description: 'Path to the migrated output DOCX.' },
|
|
35
|
+
choices: {
|
|
36
|
+
type: 'array',
|
|
37
|
+
description: 'Exactly one business choice for every source id returned by docx_list_migration_choices.',
|
|
38
|
+
items: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
sourceChoiceId: { type: 'string' },
|
|
42
|
+
action: {
|
|
43
|
+
type: 'string',
|
|
44
|
+
enum: ['place-content', 'keep-template-content', 'keep-template-label', 'select-template-option', 'exclude-source', 'review-source'],
|
|
45
|
+
},
|
|
46
|
+
targetChoiceId: { type: 'string', description: 'Required only when the selected action uses a baseline target.' },
|
|
47
|
+
cardinality: { type: 'string', enum: ['one', 'all'] },
|
|
48
|
+
},
|
|
49
|
+
required: ['sourceChoiceId', 'action'],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
templateCleanup: {
|
|
54
|
+
type: 'array',
|
|
55
|
+
description: 'Optional baseline-owned placeholders or example rows to clear.',
|
|
56
|
+
items: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
targetChoiceId: { type: 'string' },
|
|
60
|
+
scope: { type: 'string', enum: ['cell', 'row'] },
|
|
61
|
+
},
|
|
62
|
+
required: ['targetChoiceId', 'scope'],
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ['source', 'baseline', 'output', 'choices'],
|
|
68
|
+
additionalProperties: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const runtimeIdentitySchema = {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
command: { type: 'string' },
|
|
76
|
+
cwd: { type: 'string' },
|
|
77
|
+
},
|
|
78
|
+
required: ['command', 'cwd'],
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const migrationChoiceSchema = {
|
|
83
|
+
type: 'object',
|
|
84
|
+
properties: {
|
|
85
|
+
id: { type: 'string' },
|
|
86
|
+
kind: { type: 'string' },
|
|
87
|
+
scope: { type: 'string' },
|
|
88
|
+
text: { type: ['string', 'null'] },
|
|
89
|
+
count: { type: 'integer' },
|
|
90
|
+
requiredCardinality: { type: ['string', 'null'] },
|
|
91
|
+
context: { type: ['object', 'null'] },
|
|
92
|
+
allowedActions: { type: 'array', items: { type: 'string' } },
|
|
93
|
+
},
|
|
94
|
+
required: ['id', 'kind', 'scope', 'text', 'count', 'requiredCardinality', 'context', 'allowedActions'],
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const migrationCatalogOutputSchema = {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
tool: { const: 'docx_list_migration_choices' },
|
|
102
|
+
runtime: runtimeIdentitySchema,
|
|
103
|
+
catalog: {
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
schema: { type: 'string' },
|
|
107
|
+
pass: { type: 'boolean' },
|
|
108
|
+
sourceSha256: { type: 'string' },
|
|
109
|
+
baselineSha256: { type: 'string' },
|
|
110
|
+
sources: { type: 'array', items: migrationChoiceSchema },
|
|
111
|
+
targets: { type: 'array', items: migrationChoiceSchema },
|
|
112
|
+
},
|
|
113
|
+
required: ['schema', 'pass', 'sourceSha256', 'baselineSha256', 'sources', 'targets'],
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
required: ['tool', 'runtime', 'catalog'],
|
|
118
|
+
additionalProperties: false,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function migrationReceiptOutputSchema(tool) {
|
|
122
|
+
return {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
tool: { const: tool },
|
|
126
|
+
runtime: runtimeIdentitySchema,
|
|
127
|
+
receipt: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
schema: { type: 'string' },
|
|
131
|
+
toolVersion: { type: 'string' },
|
|
132
|
+
status: { type: 'string', enum: ['pass', 'review-required', 'failed'] },
|
|
133
|
+
pass: { type: 'boolean' },
|
|
134
|
+
reviewRequired: { type: 'boolean' },
|
|
135
|
+
outputVerified: { type: 'boolean' },
|
|
136
|
+
output: { type: ['string', 'null'] },
|
|
137
|
+
plan: { type: ['string', 'null'] },
|
|
138
|
+
failures: { type: 'array', items: { type: 'object' } },
|
|
139
|
+
},
|
|
140
|
+
required: ['schema', 'toolVersion', 'status', 'pass', 'reviewRequired', 'outputVerified', 'output', 'plan', 'failures'],
|
|
141
|
+
additionalProperties: true,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
required: ['tool', 'runtime', 'receipt'],
|
|
145
|
+
additionalProperties: false,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const tools = [
|
|
150
|
+
{
|
|
151
|
+
name: 'docx_inspect',
|
|
152
|
+
description: 'Inspect a DOCX document and return a unified structural report including placeholders, comments, anchors, tables, fields, and formatting metrics.',
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: { input: { type: 'string', description: 'Absolute or relative path to a .docx file.' } },
|
|
156
|
+
required: ['input'],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'docx_inspect_tables',
|
|
161
|
+
description: 'Inspect DOCX body tables with row, cell, merge, paragraph alignment, run font, color, underline, and text-fill details.',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: { input: { type: 'string', description: 'Absolute or relative path to a .docx file.' } },
|
|
165
|
+
required: ['input'],
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'docx_list_migration_choices',
|
|
170
|
+
description: 'List every current source item that still needs a business choice and the selectable current baseline targets. Returns opaque ids and context; it does not recommend a choice.',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
source: { type: 'string', description: 'Path to the current source DOCX.' },
|
|
175
|
+
baseline: { type: 'string', description: 'Path to the selected current baseline DOCX.' },
|
|
176
|
+
},
|
|
177
|
+
required: ['source', 'baseline'],
|
|
178
|
+
additionalProperties: false,
|
|
179
|
+
},
|
|
180
|
+
outputSchema: migrationCatalogOutputSchema,
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: 'docx_migrate_template',
|
|
184
|
+
description: 'Migrate a current DOCX into the selected baseline from one complete batch of business choices. Choices reference only opaque ids returned by docx_list_migration_choices; the tool derives all document values, coordinates, plans, and edits.',
|
|
185
|
+
inputSchema: templateMigrationInputSchema(),
|
|
186
|
+
outputSchema: migrationReceiptOutputSchema('docx_migrate_template'),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: 'docx_verify_migration',
|
|
190
|
+
description: 'Independently re-resolve the same business choices and verify a migrated DOCX against the current source and baseline. This does not trust the migration receipt.',
|
|
191
|
+
inputSchema: templateMigrationInputSchema(),
|
|
192
|
+
outputSchema: migrationReceiptOutputSchema('docx_verify_migration'),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: 'docx_compare',
|
|
196
|
+
description: 'Compare two DOCX files and report package, metric, and style differences.',
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: 'object',
|
|
199
|
+
properties: {
|
|
200
|
+
baseline: { type: 'string' },
|
|
201
|
+
updated: { type: 'string' },
|
|
202
|
+
},
|
|
203
|
+
required: ['baseline', 'updated'],
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'docx_validate_template_transform',
|
|
208
|
+
description: 'Validate whether a source DOCX template and target DOCX template are structurally compatible.',
|
|
209
|
+
inputSchema: {
|
|
210
|
+
type: 'object',
|
|
211
|
+
properties: {
|
|
212
|
+
sourceTemplate: { type: 'string' },
|
|
213
|
+
targetTemplate: { type: 'string' },
|
|
214
|
+
},
|
|
215
|
+
required: ['sourceTemplate', 'targetTemplate'],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: 'docx_export_json',
|
|
220
|
+
description: 'Export the body content of a DOCX document as structured JSON.',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
input: { type: 'string' },
|
|
225
|
+
output: { type: 'string' },
|
|
226
|
+
},
|
|
227
|
+
required: ['input'],
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
name: 'xlsx_inspect',
|
|
232
|
+
description: 'Inspect an XLSX workbook and return sheet-level metrics, used ranges, formula counts, and merged ranges.',
|
|
233
|
+
inputSchema: {
|
|
234
|
+
type: 'object',
|
|
235
|
+
properties: { input: { type: 'string' } },
|
|
236
|
+
required: ['input'],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'xlsx_export_json',
|
|
241
|
+
description: 'Export workbook sheet data from XLSX as structured JSON.',
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: 'object',
|
|
244
|
+
properties: {
|
|
245
|
+
input: { type: 'string' },
|
|
246
|
+
output: { type: 'string' },
|
|
247
|
+
resolveMergedCells: { type: 'boolean', description: 'Resolve merged cells to project values' }
|
|
248
|
+
},
|
|
249
|
+
required: ['input'],
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: 'xlsx_validate',
|
|
254
|
+
description: 'Validate an XLSX workbook package and return Open XML validation evidence.',
|
|
255
|
+
inputSchema: {
|
|
256
|
+
type: 'object',
|
|
257
|
+
properties: { input: { type: 'string', description: 'Absolute or relative path to a .xlsx file.' } },
|
|
258
|
+
required: ['input'],
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: 'pptx_inspect',
|
|
263
|
+
description: 'Inspect a PPTX file and return slide metrics and discovered placeholders.',
|
|
264
|
+
inputSchema: {
|
|
265
|
+
type: 'object',
|
|
266
|
+
properties: { input: { type: 'string' } },
|
|
267
|
+
required: ['input'],
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
name: 'pptx_inspect_detail',
|
|
272
|
+
description: 'Inspect a PPTX file and return detailed slide, shape, transform, paragraph, and run-format evidence.',
|
|
273
|
+
inputSchema: {
|
|
274
|
+
type: 'object',
|
|
275
|
+
properties: { input: { type: 'string' } },
|
|
276
|
+
required: ['input'],
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: 'pptx_export_json',
|
|
281
|
+
description: 'Export PPTX slide text and placeholder hints as structured JSON.',
|
|
282
|
+
inputSchema: {
|
|
283
|
+
type: 'object',
|
|
284
|
+
properties: {
|
|
285
|
+
input: { type: 'string' },
|
|
286
|
+
output: { type: 'string' },
|
|
287
|
+
},
|
|
288
|
+
required: ['input'],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
async function callTool(name, args) {
|
|
294
|
+
switch (name) {
|
|
295
|
+
case 'docx_inspect':
|
|
296
|
+
return createToolResult(await docxInspect(args));
|
|
297
|
+
case 'docx_inspect_tables':
|
|
298
|
+
return createToolResult(await docxInspectTables(args));
|
|
299
|
+
case 'docx_list_migration_choices':
|
|
300
|
+
return createToolResult(await docxListMigrationChoices(args));
|
|
301
|
+
case 'docx_migrate_template':
|
|
302
|
+
return createToolResult(await docxMigrateTemplate(args));
|
|
303
|
+
case 'docx_verify_migration':
|
|
304
|
+
return createToolResult(await docxVerifyMigration(args));
|
|
305
|
+
case 'docx_compare':
|
|
306
|
+
return createToolResult(await docxCompare(args));
|
|
307
|
+
case 'docx_validate_template_transform':
|
|
308
|
+
return createToolResult(await docxValidateTemplateTransform(args));
|
|
309
|
+
case 'docx_export_json':
|
|
310
|
+
return createToolResult(await docxExportJson(args));
|
|
311
|
+
case 'xlsx_inspect':
|
|
312
|
+
return createToolResult(await xlsxInspect(args));
|
|
313
|
+
case 'xlsx_export_json':
|
|
314
|
+
return createToolResult(await xlsxExportJson(args));
|
|
315
|
+
case 'xlsx_validate':
|
|
316
|
+
return createToolResult(await xlsxValidate(args));
|
|
317
|
+
case 'pptx_inspect':
|
|
318
|
+
return createToolResult(await pptxInspect(args));
|
|
319
|
+
case 'pptx_inspect_detail':
|
|
320
|
+
return createToolResult(await pptxInspectDetail(args));
|
|
321
|
+
case 'pptx_export_json':
|
|
322
|
+
return createToolResult(await pptxExportJson(args));
|
|
323
|
+
default:
|
|
324
|
+
throw Object.assign(new Error(`Unknown tool: ${name}`), { code: -32601 });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function docxInspect(args) {
|
|
329
|
+
const input = requireString(args.input, 'input');
|
|
330
|
+
const result = await runJsonCandidateChain(docxCandidates, ['inspect', input, '--json']);
|
|
331
|
+
return { tool: 'docx_inspect', runtime: commandRuntime(result), report: result.json };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function docxInspectTables(args) {
|
|
335
|
+
const input = requireString(args.input, 'input');
|
|
336
|
+
const result = await runJsonCandidateChain(docxCandidates, ['inspect-tables', input, '--json']);
|
|
337
|
+
return { tool: 'docx_inspect_tables', runtime: commandRuntime(result), report: result.json };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
async function docxListMigrationChoices(args) {
|
|
341
|
+
const source = requireString(args.source, 'source');
|
|
342
|
+
const baseline = requireString(args.baseline, 'baseline');
|
|
343
|
+
const result = await runJsonCandidateChain(docxCandidates, ['list-template-migration-choices', source, baseline]);
|
|
344
|
+
return { tool: 'docx_list_migration_choices', runtime: commandRuntime(result), catalog: result.json };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function docxMigrateTemplate(args) {
|
|
348
|
+
return runTemplateMigrationCommand('docx_migrate_template', 'migrate-template', args);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function docxVerifyMigration(args) {
|
|
352
|
+
return runTemplateMigrationCommand('docx_verify_migration', 'verify-template-migration', args);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function runTemplateMigrationCommand(tool, command, args) {
|
|
356
|
+
const source = requireString(args.source, 'source');
|
|
357
|
+
const baseline = requireString(args.baseline, 'baseline');
|
|
358
|
+
const output = requireString(args.output, 'output');
|
|
359
|
+
if (!Array.isArray(args.choices)) {
|
|
360
|
+
throw Object.assign(new Error('choices must be an array'), { code: -32602 });
|
|
361
|
+
}
|
|
362
|
+
const payload = {
|
|
363
|
+
schema: 'tiwater.docx.template-migration-business-choices/v1',
|
|
364
|
+
choices: args.choices,
|
|
365
|
+
...(Array.isArray(args.templateCleanup) ? { templateCleanup: args.templateCleanup } : {}),
|
|
366
|
+
};
|
|
367
|
+
return withTempJsonFile(payload, async choicesPath => {
|
|
368
|
+
const result = await runJsonCandidateChain(
|
|
369
|
+
docxCandidates,
|
|
370
|
+
[command, source, baseline, choicesPath, output],
|
|
371
|
+
{ allowedExitCodes: [0, 1] });
|
|
372
|
+
return { tool, runtime: commandRuntime(result), receipt: result.json };
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function docxCompare(args) {
|
|
377
|
+
const baseline = requireString(args.baseline, 'baseline');
|
|
378
|
+
const updated = requireString(args.updated, 'updated');
|
|
379
|
+
const result = await runJsonCandidateChain(docxCandidates, ['compare', baseline, updated, '--json']);
|
|
380
|
+
return { tool: 'docx_compare', runtime: commandRuntime(result), report: result.json };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async function docxValidateTemplateTransform(args) {
|
|
384
|
+
const sourceTemplate = requireString(args.sourceTemplate, 'sourceTemplate');
|
|
385
|
+
const targetTemplate = requireString(args.targetTemplate, 'targetTemplate');
|
|
386
|
+
const result = await runJsonCandidateChain(docxCandidates, ['validate-template-transform', sourceTemplate, targetTemplate, '--json']);
|
|
387
|
+
return { tool: 'docx_validate_template_transform', runtime: commandRuntime(result), report: result.json };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function docxExportJson(args) {
|
|
391
|
+
const input = requireString(args.input, 'input');
|
|
392
|
+
if (args.output) {
|
|
393
|
+
const output = requireString(args.output, 'output');
|
|
394
|
+
const result = await runCandidateChain(docxCandidates, ['export-json', input, output]);
|
|
395
|
+
return { tool: 'docx_export_json', runtime: commandRuntime(result), outputPath: output, document: await maybeReadJson(output) };
|
|
396
|
+
}
|
|
397
|
+
const result = await runCandidateChain(docxCandidates, ['export-json', input]);
|
|
398
|
+
return { tool: 'docx_export_json', runtime: commandRuntime(result), document: JSON.parse(result.stdout) };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function xlsxInspect(args) {
|
|
402
|
+
const input = requireString(args.input, 'input');
|
|
403
|
+
const result = await runJsonCandidateChain(xlsxCandidates, ['inspect', input, '--json']);
|
|
404
|
+
return { tool: 'xlsx_inspect', runtime: commandRuntime(result), report: result.json };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function xlsxExportJson(args) {
|
|
408
|
+
const input = requireString(args.input, 'input');
|
|
409
|
+
const cmdArgs = ['export-json', input];
|
|
410
|
+
if (args.resolveMergedCells) {
|
|
411
|
+
cmdArgs.push('--resolve-merged-cells');
|
|
412
|
+
}
|
|
413
|
+
if (args.output) {
|
|
414
|
+
const output = requireString(args.output, 'output');
|
|
415
|
+
cmdArgs.push(output);
|
|
416
|
+
const result = await runCandidateChain(xlsxCandidates, cmdArgs);
|
|
417
|
+
return { tool: 'xlsx_export_json', runtime: commandRuntime(result), outputPath: output, workbook: await maybeReadJson(output) };
|
|
418
|
+
}
|
|
419
|
+
const result = await runCandidateChain(xlsxCandidates, cmdArgs);
|
|
420
|
+
return { tool: 'xlsx_export_json', runtime: commandRuntime(result), workbook: JSON.parse(result.stdout) };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function xlsxValidate(args) {
|
|
424
|
+
const input = requireString(args.input, 'input');
|
|
425
|
+
const result = await runXlsxValidateCandidateChain(['validate', input]);
|
|
426
|
+
return { tool: 'xlsx_validate', runtime: commandRuntime(result), result: result.json };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function pptxInspect(args) {
|
|
430
|
+
const input = requireString(args.input, 'input');
|
|
431
|
+
const result = await runJsonCandidateChain(pptxCandidates, ['inspect', input, '--json']);
|
|
432
|
+
return { tool: 'pptx_inspect', runtime: commandRuntime(result), report: result.json };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function pptxInspectDetail(args) {
|
|
436
|
+
const input = requireString(args.input, 'input');
|
|
437
|
+
const result = await runJsonCandidateChain(pptxCandidates, ['inspect', input, '--json', '--detail']);
|
|
438
|
+
return { tool: 'pptx_inspect_detail', runtime: commandRuntime(result), report: result.json };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function pptxExportJson(args) {
|
|
442
|
+
const input = requireString(args.input, 'input');
|
|
443
|
+
if (args.output) {
|
|
444
|
+
const output = requireString(args.output, 'output');
|
|
445
|
+
const result = await runCandidateChain(pptxCandidates, ['export-json', input, output]);
|
|
446
|
+
return { tool: 'pptx_export_json', runtime: commandRuntime(result), outputPath: output, document: await maybeReadJson(output) };
|
|
447
|
+
}
|
|
448
|
+
const result = await runCandidateChain(pptxCandidates, ['export-json', input]);
|
|
449
|
+
return { tool: 'pptx_export_json', runtime: commandRuntime(result), document: JSON.parse(result.stdout) };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function commandRuntime(result) {
|
|
453
|
+
return {
|
|
454
|
+
command: result.command,
|
|
455
|
+
cwd: result.cwd || path.dirname(result.command),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await new McpStdioServer({ name: 'tiwater-office', version: '0.2.0', tools, callTool }).start();
|
|
460
|
+
|
|
461
|
+
async function runXlsxValidateCandidateChain(args) {
|
|
462
|
+
const errors = [];
|
|
463
|
+
for (const candidate of xlsxCandidates) {
|
|
464
|
+
try {
|
|
465
|
+
const result = await runValidationCommand(candidate, args);
|
|
466
|
+
const text = result.stdout.trim();
|
|
467
|
+
if (!text) return { ...result, json: null };
|
|
468
|
+
try {
|
|
469
|
+
return { ...result, json: JSON.parse(text) };
|
|
470
|
+
} catch {
|
|
471
|
+
if (result.code !== 0) {
|
|
472
|
+
errors.push(`${candidate.command}: validate did not return JSON`);
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
throw new Error(`Expected JSON output but received: ${text.slice(0, 300)}${text.length > 300 ? '…' : ''}`);
|
|
476
|
+
}
|
|
477
|
+
} catch (error) {
|
|
478
|
+
if (error?.code === 'ENOENT') {
|
|
479
|
+
errors.push(`${candidate.command}: not found`);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
throw error;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
throw new Error(`No runnable command candidate succeeded. ${errors.join('; ')}`);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function runValidationCommand(candidate, args) {
|
|
489
|
+
const env = { ...process.env, ...(candidate.env || {}) };
|
|
490
|
+
const cwd = candidate.cwd || resolveRepoPath();
|
|
491
|
+
const commandArgs = [...(candidate.argsPrefix || []), ...args];
|
|
492
|
+
|
|
493
|
+
return await new Promise((resolve, reject) => {
|
|
494
|
+
const child = spawn(candidate.command, commandArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
495
|
+
let stdout = '';
|
|
496
|
+
let stderr = '';
|
|
497
|
+
|
|
498
|
+
child.stdout.on('data', chunk => {
|
|
499
|
+
stdout += chunk.toString();
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
child.stderr.on('data', chunk => {
|
|
503
|
+
stderr += chunk.toString();
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
child.on('error', reject);
|
|
507
|
+
child.on('close', code => {
|
|
508
|
+
if (code === 0 || code === 1) {
|
|
509
|
+
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
reject(new Error(`${candidate.command} ${commandArgs.join(' ')} failed with exit code ${code}\n${stderr || stdout}`));
|
|
513
|
+
});
|
|
514
|
+
});
|
|
515
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tiwater/office-mcp",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Published MCP server for Tiwater Office document capabilities",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/tiwater/mcp-servers",
|
|
10
|
+
"directory": "servers"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"tiwater-office-mcp": "office/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"_shared/*.mjs",
|
|
17
|
+
"office/index.mjs",
|
|
18
|
+
"office/README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test office/*.test.mjs"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
}
|
|
26
|
+
}
|