@tiwater/office-mcp 0.21.27 → 0.21.29
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/large-json-result.mjs +2 -1
- package/_shared/mcp-stdio.mjs +154 -0
- package/office/contracts/tiwater-office-provider-contract-manifest-v1.json +12 -1
- package/office/contracts/xlsx_read_range.schema.json +47 -0
- package/office/index.mjs +84 -0
- package/package.json +9 -4
- package/pdf/README.md +30 -0
- package/pdf/contracts/pdf_extract_table_details.schema.json +37 -0
- package/pdf/contracts/pdf_extract_tables.schema.json +40 -0
- package/pdf/contracts/pdf_find_table.schema.json +36 -0
- package/pdf/contracts/pdf_inspect.schema.json +28 -0
- package/pdf/contracts/pdf_ocr.schema.json +37 -0
- package/pdf/contracts/tiwater-pdf-provider-contract-manifest-v1.json +47 -0
- package/pdf/index.mjs +307 -0
|
@@ -16,7 +16,7 @@ export function resultChannels(args) {
|
|
|
16
16
|
return { returnContent, output };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export async function deliverLargeJsonResult({ tool, args, runtime, payload, sourcePaths, summary }) {
|
|
19
|
+
export async function deliverLargeJsonResult({ tool, args, runtime, payload, sourcePaths, summary, identity }) {
|
|
20
20
|
const channels = resultChannels(args);
|
|
21
21
|
const contentBytes = Buffer.byteLength(JSON.stringify(payload), 'utf8');
|
|
22
22
|
const contentReturned = channels.returnContent && contentBytes <= returnedContentBudgetBytes;
|
|
@@ -38,6 +38,7 @@ export async function deliverLargeJsonResult({ tool, args, runtime, payload, sou
|
|
|
38
38
|
contentWritten: channels.output !== null,
|
|
39
39
|
},
|
|
40
40
|
...(summary === undefined ? {} : { summary }),
|
|
41
|
+
...(identity === undefined ? {} : { identity }),
|
|
41
42
|
...(contentReturned ? { content: payload } : {}),
|
|
42
43
|
};
|
|
43
44
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
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 (Number.isInteger(error.code) && error.message) {
|
|
18
|
+
return toError(error.code, error.message, error.data);
|
|
19
|
+
}
|
|
20
|
+
return toError(-32603, error instanceof Error ? error.message : String(error));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class McpStdioServer {
|
|
24
|
+
constructor({ name, version, instructions, tools, callTool, logger = console.error }) {
|
|
25
|
+
this.serverInfo = { name, version };
|
|
26
|
+
this.instructions = instructions;
|
|
27
|
+
this.tools = tools;
|
|
28
|
+
this.callTool = callTool;
|
|
29
|
+
this.logger = logger;
|
|
30
|
+
this.lineBuffer = '';
|
|
31
|
+
this.binaryBuffer = Buffer.alloc(0);
|
|
32
|
+
this.initialized = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
start() {
|
|
36
|
+
process.stdin.on('data', chunk => this.#onData(chunk));
|
|
37
|
+
process.stdin.on('end', () => process.exit(0));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#onData(chunk) {
|
|
41
|
+
const text = chunk.toString('utf8');
|
|
42
|
+
|
|
43
|
+
if (this.binaryBuffer.length > 0 || text.includes('Content-Length:')) {
|
|
44
|
+
this.binaryBuffer = Buffer.concat([this.binaryBuffer, chunk]);
|
|
45
|
+
this.#drainContentLengthBuffer();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this.lineBuffer += text;
|
|
50
|
+
while (true) {
|
|
51
|
+
const newlineIndex = this.lineBuffer.indexOf('\n');
|
|
52
|
+
if (newlineIndex === -1) return;
|
|
53
|
+
const line = this.lineBuffer.slice(0, newlineIndex).replace(/\r$/, '').trim();
|
|
54
|
+
this.lineBuffer = this.lineBuffer.slice(newlineIndex + 1);
|
|
55
|
+
if (!line) continue;
|
|
56
|
+
this.#parseAndHandle(line, null);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#drainContentLengthBuffer() {
|
|
61
|
+
while (true) {
|
|
62
|
+
const headerEnd = this.binaryBuffer.indexOf('\r\n\r\n');
|
|
63
|
+
if (headerEnd === -1) return;
|
|
64
|
+
|
|
65
|
+
const headerText = this.binaryBuffer.subarray(0, headerEnd).toString('utf8');
|
|
66
|
+
const lengthMatch = headerText.match(/Content-Length:\s*(\d+)/i);
|
|
67
|
+
if (!lengthMatch) {
|
|
68
|
+
this.logger('Missing Content-Length header');
|
|
69
|
+
this.binaryBuffer = Buffer.alloc(0);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const contentLength = Number(lengthMatch[1]);
|
|
74
|
+
const messageStart = headerEnd + 4;
|
|
75
|
+
const messageEnd = messageStart + contentLength;
|
|
76
|
+
if (this.binaryBuffer.length < messageEnd) return;
|
|
77
|
+
|
|
78
|
+
const body = this.binaryBuffer.subarray(messageStart, messageEnd).toString('utf8');
|
|
79
|
+
this.binaryBuffer = this.binaryBuffer.subarray(messageEnd);
|
|
80
|
+
this.#parseAndHandle(body, null);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#parseAndHandle(body, idHint) {
|
|
85
|
+
let message;
|
|
86
|
+
try {
|
|
87
|
+
message = JSON.parse(body);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
writeMessage({ jsonrpc: JSONRPC_VERSION, id: idHint, error: toError(-32700, 'Parse error', String(error)) });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
void this.#handleMessage(message);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async #handleMessage(message) {
|
|
97
|
+
if (!message || message.jsonrpc !== JSONRPC_VERSION || typeof message.method !== 'string') {
|
|
98
|
+
if ('id' in (message || {})) {
|
|
99
|
+
writeMessage({ jsonrpc: JSONRPC_VERSION, id: message.id ?? null, error: toError(-32600, 'Invalid Request') });
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { id, method, params = {} } = message;
|
|
105
|
+
const isNotification = id === undefined;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
switch (method) {
|
|
109
|
+
case 'initialize': {
|
|
110
|
+
const requested = params.protocolVersion;
|
|
111
|
+
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : FALLBACK_PROTOCOL_VERSION;
|
|
112
|
+
const result = {
|
|
113
|
+
protocolVersion,
|
|
114
|
+
capabilities: { tools: {} },
|
|
115
|
+
serverInfo: this.serverInfo,
|
|
116
|
+
...(this.instructions ? { instructions: this.instructions } : {}),
|
|
117
|
+
};
|
|
118
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
case 'notifications/initialized': {
|
|
122
|
+
this.initialized = true;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
case 'ping': {
|
|
126
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result: {} });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
case 'tools/list': {
|
|
130
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result: { tools: this.tools } });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
case 'tools/call': {
|
|
134
|
+
const name = params?.name;
|
|
135
|
+
const args = params?.arguments ?? {};
|
|
136
|
+
if (typeof name !== 'string' || !name) {
|
|
137
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, error: toError(-32602, 'Invalid params: missing tool name') });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const result = await this.callTool(name, args);
|
|
141
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, result });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
default: {
|
|
145
|
+
if (!isNotification) writeMessage({ jsonrpc: JSONRPC_VERSION, id, error: toError(-32601, `Method not found: ${method}`) });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (!isNotification) {
|
|
150
|
+
writeMessage({ jsonrpc: JSONRPC_VERSION, id, error: normalizeToolCallError(error) });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema": "tiwater.office-provider-contract-manifest/v1",
|
|
3
3
|
"provider": {
|
|
4
4
|
"id": "@tiwater/office-mcp",
|
|
5
|
-
"version": "0.21.
|
|
5
|
+
"version": "0.21.29"
|
|
6
6
|
},
|
|
7
7
|
"tools": [
|
|
8
8
|
{
|
|
@@ -456,6 +456,17 @@
|
|
|
456
456
|
"sha256": "9ae4a846a8ca5b9059dcfd2a589e9872a2ab8d4ba3a118a513f3e05462f923d0"
|
|
457
457
|
}
|
|
458
458
|
},
|
|
459
|
+
{
|
|
460
|
+
"name": "xlsx_read_range",
|
|
461
|
+
"providerContract": {
|
|
462
|
+
"source": "packages/xlsx-cli/contracts/mcp-input/xlsx_read_range.schema.json",
|
|
463
|
+
"sha256": "04b0ce1b912047f774fbbbb6ba7cf55e5e53a9b74d6f027d9c21b4d6d7397225"
|
|
464
|
+
},
|
|
465
|
+
"inputContract": {
|
|
466
|
+
"path": "office/contracts/xlsx_read_range.schema.json",
|
|
467
|
+
"sha256": "04b0ce1b912047f774fbbbb6ba7cf55e5e53a9b74d6f027d9c21b4d6d7397225"
|
|
468
|
+
}
|
|
469
|
+
},
|
|
459
470
|
{
|
|
460
471
|
"name": "xlsx_set_cell_number_format",
|
|
461
472
|
"providerContract": {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "tiwater.office-mcp-input/xlsx_read_range/v1",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"input": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1,
|
|
9
|
+
"description": "Current XLSX workbook. Convert legacy XLS before selecting Open XML cells.",
|
|
10
|
+
"x-tiwater-file-role": "read"
|
|
11
|
+
},
|
|
12
|
+
"sheet": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"minLength": 1,
|
|
15
|
+
"description": "Exact current worksheet name returned by workbook observation."
|
|
16
|
+
},
|
|
17
|
+
"range": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"pattern": "^[A-Za-z]{1,3}[1-9][0-9]*(?::[A-Za-z]{1,3}[1-9][0-9]*)?$",
|
|
20
|
+
"description": "Explicit native A1 cell or rectangular range. A single cell is a one-cell range."
|
|
21
|
+
},
|
|
22
|
+
"offset": {
|
|
23
|
+
"type": "integer",
|
|
24
|
+
"minimum": 0,
|
|
25
|
+
"maximum": 9007199254740991,
|
|
26
|
+
"description": "Zero-based cell offset in row-major order within the selected range. Continue only when another selected cell is needed."
|
|
27
|
+
},
|
|
28
|
+
"limit": {
|
|
29
|
+
"type": "integer",
|
|
30
|
+
"minimum": 1,
|
|
31
|
+
"maximum": 256,
|
|
32
|
+
"description": "Maximum selected cells in this bounded page."
|
|
33
|
+
},
|
|
34
|
+
"returnContent": {
|
|
35
|
+
"type": "boolean",
|
|
36
|
+
"description": "Return this selected technical page directly when it fits the response limit. May be combined with output."
|
|
37
|
+
},
|
|
38
|
+
"output": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"minLength": 1,
|
|
41
|
+
"description": "Optional immutable JSON artifact path for this selected technical page. May be combined with returnContent.",
|
|
42
|
+
"x-tiwater-file-role": "write"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"required": ["input", "sheet", "range", "limit", "returnContent"],
|
|
46
|
+
"additionalProperties": false
|
|
47
|
+
}
|
package/office/index.mjs
CHANGED
|
@@ -202,6 +202,67 @@ const xlsxInspectionSummary = z.object({
|
|
|
202
202
|
}).strict()).max(6),
|
|
203
203
|
}).strict();
|
|
204
204
|
|
|
205
|
+
const xlsxRangePageSummary = z.object({
|
|
206
|
+
schema: z.literal('tiwater.xlsx-range-page-receipt/v1'),
|
|
207
|
+
totalCellCount: z.number().int().nonnegative(),
|
|
208
|
+
returnedCellCount: z.number().int().nonnegative(),
|
|
209
|
+
remaining: z.number().int().nonnegative(),
|
|
210
|
+
nextOffset: z.number().int().nonnegative().nullable(),
|
|
211
|
+
}).strict();
|
|
212
|
+
|
|
213
|
+
const xlsxRangePage = z.object({
|
|
214
|
+
schema: z.literal('tiwater.xlsx-range-page/v1'),
|
|
215
|
+
toolVersion: z.string(),
|
|
216
|
+
file: z.string(),
|
|
217
|
+
inputSha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
218
|
+
sheet: z.string(),
|
|
219
|
+
range: z.string(),
|
|
220
|
+
receipt: xlsxRangePageSummary,
|
|
221
|
+
cells: z.array(z.object({
|
|
222
|
+
reference: z.string(),
|
|
223
|
+
row: z.number().int().positive(),
|
|
224
|
+
column: z.number().int().positive(),
|
|
225
|
+
physical: z.boolean(),
|
|
226
|
+
rawValue: z.string().nullable(),
|
|
227
|
+
formattedValue: z.string().nullable(),
|
|
228
|
+
valueType: z.string().nullable(),
|
|
229
|
+
normalizedValue: z.object({ kind: z.string(), iso8601: z.string().nullable() }).strict().nullable(),
|
|
230
|
+
formula: z.object({
|
|
231
|
+
text: z.string(),
|
|
232
|
+
type: z.string().nullable(),
|
|
233
|
+
sharedIndex: z.number().int().nonnegative().nullable(),
|
|
234
|
+
reference: z.string().nullable(),
|
|
235
|
+
}).strict().nullable(),
|
|
236
|
+
style: z.object({
|
|
237
|
+
styleIndex: z.number().int().nonnegative(),
|
|
238
|
+
numberFormatId: z.number().int().nonnegative(),
|
|
239
|
+
numberFormatCode: z.string().nullable(),
|
|
240
|
+
fontId: z.number().int().nonnegative(),
|
|
241
|
+
fillId: z.number().int().nonnegative(),
|
|
242
|
+
borderId: z.number().int().nonnegative(),
|
|
243
|
+
horizontalAlignment: z.string().nullable(),
|
|
244
|
+
verticalAlignment: z.string().nullable(),
|
|
245
|
+
wrapText: z.boolean(),
|
|
246
|
+
bold: z.boolean(),
|
|
247
|
+
}).strict().nullable(),
|
|
248
|
+
richTextRuns: z.array(z.object({
|
|
249
|
+
text: z.string(),
|
|
250
|
+
fontName: z.string().nullable(),
|
|
251
|
+
color: z.string().nullable(),
|
|
252
|
+
underline: z.string().nullable(),
|
|
253
|
+
bold: z.boolean(),
|
|
254
|
+
italic: z.boolean(),
|
|
255
|
+
}).strict()).nullable(),
|
|
256
|
+
mergedRange: z.string().nullable(),
|
|
257
|
+
mergeOwner: z.string().nullable(),
|
|
258
|
+
}).strict()).max(256),
|
|
259
|
+
}).strict();
|
|
260
|
+
|
|
261
|
+
const xlsxRangeReadOutput = largeResultOutput('xlsx_read_range').extend({
|
|
262
|
+
summary: xlsxRangePageSummary,
|
|
263
|
+
content: xlsxRangePage.optional(),
|
|
264
|
+
}).strict();
|
|
265
|
+
|
|
205
266
|
const pptxInspectionSummary = z.object({
|
|
206
267
|
slideCount: z.number().int().nonnegative(),
|
|
207
268
|
masterCount: z.number().int().nonnegative(),
|
|
@@ -580,6 +641,14 @@ const tools = [
|
|
|
580
641
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
581
642
|
handler: xlsxExportJson,
|
|
582
643
|
},
|
|
644
|
+
{
|
|
645
|
+
name: 'xlsx_read_range',
|
|
646
|
+
description: 'Read one explicit native A1 cell or rectangular range from one current XLSX worksheet. Pages use a row-major cell offset and return physical presence, raw and formatted values, normalized value type, formula metadata, style, rich text, and merged-range ownership. The receipt always reports remaining cells and the next offset. Continue only when another selected cell is needed. Set returnContent true to return the selected page when it fits the response limit. Provide output to store the same complete selected page as an immutable artifact. These channels are independent and may be used together; at least one is required. This tool does not infer regions, headers, records, field meanings, or business mappings; convert legacy XLS before reading Open XML cells.',
|
|
647
|
+
inputSchema: inputContract('xlsx_read_range'),
|
|
648
|
+
outputSchema: xlsxRangeReadOutput,
|
|
649
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
650
|
+
handler: xlsxReadRange,
|
|
651
|
+
},
|
|
583
652
|
...fixedToolDefinitions(xlsxFixedTools),
|
|
584
653
|
{
|
|
585
654
|
name: 'xlsx_validate',
|
|
@@ -1202,6 +1271,21 @@ async function xlsxExportJson(args) {
|
|
|
1202
1271
|
return deliverLargeJsonResult({ tool: 'xlsx_export_json', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input] });
|
|
1203
1272
|
}
|
|
1204
1273
|
|
|
1274
|
+
async function xlsxReadRange(args) {
|
|
1275
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
1276
|
+
return withTempJsonFile(args, async requestPath => {
|
|
1277
|
+
const result = await runJsonCandidateChain(xlsxCandidates, ['xlsx_read_range', requestPath]);
|
|
1278
|
+
return deliverLargeJsonResult({
|
|
1279
|
+
tool: 'xlsx_read_range',
|
|
1280
|
+
args,
|
|
1281
|
+
runtime: commandRuntime(result),
|
|
1282
|
+
payload: result.json,
|
|
1283
|
+
sourcePaths: [input],
|
|
1284
|
+
summary: result.json.receipt,
|
|
1285
|
+
});
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1205
1289
|
async function xlsxValidate(args) {
|
|
1206
1290
|
const input = path.resolve(requireString(args.input, 'input'));
|
|
1207
1291
|
const result = await runJsonCandidateChain(xlsxCandidates, ['validate', input], { allowedExitCodes: [0, 1] });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiwater/office-mcp",
|
|
3
|
-
"version": "0.21.
|
|
4
|
-
"description": "Published MCP
|
|
3
|
+
"version": "0.21.29",
|
|
4
|
+
"description": "Published MCP distribution for independent Tiwater Office and PDF document capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"directory": "servers"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
|
-
"tiwater-office-mcp": "office/index.mjs"
|
|
13
|
+
"tiwater-office-mcp": "office/index.mjs",
|
|
14
|
+
"tiwater-pdf-mcp": "pdf/index.mjs"
|
|
14
15
|
},
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=20"
|
|
@@ -19,11 +20,15 @@
|
|
|
19
20
|
"_shared/tool-runtime.mjs",
|
|
20
21
|
"_shared/large-json-result.mjs",
|
|
21
22
|
"_shared/output-write-lock.mjs",
|
|
23
|
+
"_shared/mcp-stdio.mjs",
|
|
22
24
|
"office/index.mjs",
|
|
23
25
|
"office/docx-object-identity.mjs",
|
|
24
26
|
"office/README.md",
|
|
25
27
|
"office/contracts/tiwater-office-provider-contract-manifest-v1.json",
|
|
26
|
-
"office/contracts/*.schema.json"
|
|
28
|
+
"office/contracts/*.schema.json",
|
|
29
|
+
"pdf/index.mjs",
|
|
30
|
+
"pdf/README.md",
|
|
31
|
+
"pdf/contracts/*.json"
|
|
27
32
|
],
|
|
28
33
|
"publishConfig": {
|
|
29
34
|
"access": "public"
|
package/pdf/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Tiwater PDF MCP
|
|
2
|
+
|
|
3
|
+
Published, Agent-facing stdio MCP server for PDF inspection, table extraction,
|
|
4
|
+
and OCR. It is a separate executable, process, and tool surface from the Office
|
|
5
|
+
MCP, distributed in the same npm package so both use one trusted release path.
|
|
6
|
+
|
|
7
|
+
## Tools
|
|
8
|
+
|
|
9
|
+
- `pdf_inspect`
|
|
10
|
+
- `pdf_extract_tables`
|
|
11
|
+
- `pdf_find_table`
|
|
12
|
+
- `pdf_extract_table_details`
|
|
13
|
+
- `pdf_ocr` (pinned Aliyun `qwen3.8-max`; per-invocation Supen credential)
|
|
14
|
+
|
|
15
|
+
Every tool is read-only and records the exact input PDF identity. Read results
|
|
16
|
+
can be retained in immutable JSON artifacts. `pdf_inspect` always requires that
|
|
17
|
+
durable artifact and separately returns a bounded document identity without
|
|
18
|
+
traversing page content.
|
|
19
|
+
|
|
20
|
+
## Install and run
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install --global @tiwater/office-mcp
|
|
24
|
+
tiwater-pdf-mcp
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The server invokes the independently published `tiwater-pdf` executable from
|
|
28
|
+
`PATH`. It does not fall back to a repository checkout. OCR credentials are
|
|
29
|
+
provided to that child for one invocation by the runtime environment; the MCP
|
|
30
|
+
does not read or persist provider credentials.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"input": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"minLength": 1,
|
|
8
|
+
"description": "Path to the current PDF revision.",
|
|
9
|
+
"x-tiwater-file-role": "read"
|
|
10
|
+
},
|
|
11
|
+
"returnContent": {
|
|
12
|
+
"type": "boolean",
|
|
13
|
+
"description": "Return the complete result directly when it fits the published response limit."
|
|
14
|
+
},
|
|
15
|
+
"output": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "Absolute path for the immutable JSON observation artifact. An identical replay may reuse identical bytes; different content is never written over it.",
|
|
19
|
+
"x-tiwater-file-role": "write",
|
|
20
|
+
"x-tiwater-file-effect": false
|
|
21
|
+
},
|
|
22
|
+
"pages": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"uniqueItems": true,
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"minimum": 1
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"required": [
|
|
33
|
+
"input",
|
|
34
|
+
"returnContent"
|
|
35
|
+
],
|
|
36
|
+
"additionalProperties": false
|
|
37
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"input": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"minLength": 1,
|
|
8
|
+
"description": "Path to the current PDF revision.",
|
|
9
|
+
"x-tiwater-file-role": "read"
|
|
10
|
+
},
|
|
11
|
+
"returnContent": {
|
|
12
|
+
"type": "boolean",
|
|
13
|
+
"description": "Return the complete result directly when it fits the published response limit."
|
|
14
|
+
},
|
|
15
|
+
"output": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "Absolute path for the immutable JSON observation artifact. An identical replay may reuse identical bytes; different content is never written over it.",
|
|
19
|
+
"x-tiwater-file-role": "write",
|
|
20
|
+
"x-tiwater-file-effect": false
|
|
21
|
+
},
|
|
22
|
+
"pages": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"uniqueItems": true,
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"minimum": 1
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"autoSpan": {
|
|
32
|
+
"type": "boolean"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": [
|
|
36
|
+
"input",
|
|
37
|
+
"returnContent"
|
|
38
|
+
],
|
|
39
|
+
"additionalProperties": false
|
|
40
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"input": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"minLength": 1,
|
|
8
|
+
"description": "Path to the current PDF revision.",
|
|
9
|
+
"x-tiwater-file-role": "read"
|
|
10
|
+
},
|
|
11
|
+
"returnContent": {
|
|
12
|
+
"type": "boolean",
|
|
13
|
+
"description": "Return the complete result directly when it fits the published response limit."
|
|
14
|
+
},
|
|
15
|
+
"output": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "Absolute path for the immutable JSON observation artifact. An identical replay may reuse identical bytes; different content is never written over it.",
|
|
19
|
+
"x-tiwater-file-role": "write",
|
|
20
|
+
"x-tiwater-file-effect": false
|
|
21
|
+
},
|
|
22
|
+
"name": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"minLength": 1
|
|
25
|
+
},
|
|
26
|
+
"autoSpan": {
|
|
27
|
+
"type": "boolean"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"required": [
|
|
31
|
+
"input",
|
|
32
|
+
"name",
|
|
33
|
+
"returnContent"
|
|
34
|
+
],
|
|
35
|
+
"additionalProperties": false
|
|
36
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"input": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"minLength": 1,
|
|
8
|
+
"description": "Path to the current PDF revision.",
|
|
9
|
+
"x-tiwater-file-role": "read"
|
|
10
|
+
},
|
|
11
|
+
"returnContent": {
|
|
12
|
+
"type": "boolean",
|
|
13
|
+
"description": "Return the complete result directly when it fits the published response limit."
|
|
14
|
+
},
|
|
15
|
+
"output": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "Absolute path for the immutable JSON observation artifact. An identical replay may reuse identical bytes; different content is never written over it.",
|
|
19
|
+
"x-tiwater-file-role": "write",
|
|
20
|
+
"x-tiwater-file-effect": false
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"required": [
|
|
24
|
+
"input",
|
|
25
|
+
"output"
|
|
26
|
+
],
|
|
27
|
+
"additionalProperties": false
|
|
28
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"input": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"minLength": 1,
|
|
8
|
+
"description": "Path to the current PDF revision.",
|
|
9
|
+
"x-tiwater-file-role": "read"
|
|
10
|
+
},
|
|
11
|
+
"returnContent": {
|
|
12
|
+
"type": "boolean",
|
|
13
|
+
"description": "Return the complete result directly when it fits the published response limit."
|
|
14
|
+
},
|
|
15
|
+
"output": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "Absolute path for the immutable JSON observation artifact. An identical replay may reuse identical bytes; different content is never written over it.",
|
|
19
|
+
"x-tiwater-file-role": "write",
|
|
20
|
+
"x-tiwater-file-effect": false
|
|
21
|
+
},
|
|
22
|
+
"pages": {
|
|
23
|
+
"type": "array",
|
|
24
|
+
"minItems": 1,
|
|
25
|
+
"uniqueItems": true,
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "integer",
|
|
28
|
+
"minimum": 1
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"required": [
|
|
33
|
+
"input",
|
|
34
|
+
"returnContent"
|
|
35
|
+
],
|
|
36
|
+
"additionalProperties": false
|
|
37
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "tiwater.pdf-provider-contract-manifest/v1",
|
|
3
|
+
"provider": {
|
|
4
|
+
"id": "@tiwater/office-mcp",
|
|
5
|
+
"version": "0.21.29"
|
|
6
|
+
},
|
|
7
|
+
"runtime": {
|
|
8
|
+
"command": "tiwater-pdf"
|
|
9
|
+
},
|
|
10
|
+
"tools": [
|
|
11
|
+
{
|
|
12
|
+
"name": "pdf_inspect",
|
|
13
|
+
"inputContract": {
|
|
14
|
+
"path": "contracts/pdf_inspect.schema.json",
|
|
15
|
+
"sha256": "0b397cca1351bfe79af8bb2b81e107c9d566f7772f3c6f7eea54d26048116eeb"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"name": "pdf_extract_tables",
|
|
20
|
+
"inputContract": {
|
|
21
|
+
"path": "contracts/pdf_extract_tables.schema.json",
|
|
22
|
+
"sha256": "2ff2d609964e0d2c6cc84c8f8caf6002de3ce8ce4b78e28a99a9e0430502d3b4"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"name": "pdf_find_table",
|
|
27
|
+
"inputContract": {
|
|
28
|
+
"path": "contracts/pdf_find_table.schema.json",
|
|
29
|
+
"sha256": "1bb441cedaeb78a3776096718d1ce51f29e7832e19ed0a43f9138c45a400bd18"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "pdf_ocr",
|
|
34
|
+
"inputContract": {
|
|
35
|
+
"path": "contracts/pdf_ocr.schema.json",
|
|
36
|
+
"sha256": "8910ece3646ca0154ab8d18bd602d2fc9666067543d4cdc2b999a8d8543aada2"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "pdf_extract_table_details",
|
|
41
|
+
"inputContract": {
|
|
42
|
+
"path": "contracts/pdf_extract_table_details.schema.json",
|
|
43
|
+
"sha256": "8910ece3646ca0154ab8d18bd602d2fc9666067543d4cdc2b999a8d8543aada2"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
package/pdf/index.mjs
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import { deliverLargeJsonResult } from '../_shared/large-json-result.mjs';
|
|
9
|
+
import { McpStdioServer } from '../_shared/mcp-stdio.mjs';
|
|
10
|
+
import {
|
|
11
|
+
commandCandidate,
|
|
12
|
+
createToolResult,
|
|
13
|
+
requireString,
|
|
14
|
+
runJsonCandidateChain,
|
|
15
|
+
} from '../_shared/tool-runtime.mjs';
|
|
16
|
+
|
|
17
|
+
const pdfRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const distributionRoot = path.resolve(pdfRoot, '..');
|
|
19
|
+
const packageJson = readJson(path.join(distributionRoot, 'package.json'));
|
|
20
|
+
const manifest = readJson(path.join(
|
|
21
|
+
pdfRoot, 'contracts', 'tiwater-pdf-provider-contract-manifest-v1.json',
|
|
22
|
+
));
|
|
23
|
+
if (manifest.schema !== 'tiwater.pdf-provider-contract-manifest/v1'
|
|
24
|
+
|| manifest.provider?.id !== packageJson.name
|
|
25
|
+
|| manifest.provider?.version !== packageJson.version) {
|
|
26
|
+
throw new Error('pdf-provider-contract-manifest-invalid');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const pdfCandidates = [commandCandidate('tiwater-pdf')];
|
|
30
|
+
const artifactSchema = {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
path: { type: 'string' },
|
|
34
|
+
sha256: { type: 'string', pattern: '^[0-9a-f]{64}$' },
|
|
35
|
+
bytes: { type: 'integer', minimum: 1 },
|
|
36
|
+
},
|
|
37
|
+
required: ['path', 'sha256', 'bytes'],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
};
|
|
40
|
+
const receiptSchema = {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
contentBytes: { type: 'integer', minimum: 0 },
|
|
44
|
+
contentReturned: { type: 'boolean' },
|
|
45
|
+
contentWritten: { type: 'boolean' },
|
|
46
|
+
},
|
|
47
|
+
required: ['contentBytes', 'contentReturned', 'contentWritten'],
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
};
|
|
50
|
+
const largeResultOutputSchema = {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
tool: { type: 'string' },
|
|
54
|
+
runtime: {
|
|
55
|
+
type: 'object',
|
|
56
|
+
properties: {
|
|
57
|
+
command: { type: 'string' },
|
|
58
|
+
arguments: { type: 'array', items: { type: 'string' } },
|
|
59
|
+
},
|
|
60
|
+
required: ['command', 'arguments'],
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
},
|
|
63
|
+
sources: { type: 'array', minItems: 1, maxItems: 1, items: artifactSchema },
|
|
64
|
+
returnContent: { type: 'boolean' },
|
|
65
|
+
artifact: { anyOf: [artifactSchema, { type: 'null' }] },
|
|
66
|
+
receipt: receiptSchema,
|
|
67
|
+
content: {},
|
|
68
|
+
},
|
|
69
|
+
required: ['tool', 'runtime', 'sources', 'returnContent', 'artifact', 'receipt'],
|
|
70
|
+
additionalProperties: false,
|
|
71
|
+
};
|
|
72
|
+
const pageIdentitySchema = {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
page: { type: 'integer', minimum: 1 },
|
|
76
|
+
width: { type: 'number', exclusiveMinimum: 0 },
|
|
77
|
+
height: { type: 'number', exclusiveMinimum: 0 },
|
|
78
|
+
imageCount: { type: 'integer', minimum: 0 },
|
|
79
|
+
wordCount: { type: 'integer', minimum: 0 },
|
|
80
|
+
imageOnly: { type: 'boolean' },
|
|
81
|
+
},
|
|
82
|
+
required: ['page', 'width', 'height', 'imageCount', 'wordCount', 'imageOnly'],
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
};
|
|
85
|
+
const inspectIdentitySchema = {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: {
|
|
88
|
+
format: { const: 'pdf' },
|
|
89
|
+
pageCount: { type: 'integer', minimum: 1 },
|
|
90
|
+
title: { type: ['string', 'null'] },
|
|
91
|
+
author: { type: ['string', 'null'] },
|
|
92
|
+
subject: { type: ['string', 'null'] },
|
|
93
|
+
imageCount: { type: 'integer', minimum: 0 },
|
|
94
|
+
wordCount: { type: 'integer', minimum: 0 },
|
|
95
|
+
scannedPageCount: { type: 'integer', minimum: 0 },
|
|
96
|
+
imageOnly: { type: 'boolean' },
|
|
97
|
+
openingPages: { type: 'array', maxItems: 3, items: pageIdentitySchema },
|
|
98
|
+
},
|
|
99
|
+
required: [
|
|
100
|
+
'format', 'pageCount', 'title', 'author', 'subject', 'imageCount', 'wordCount',
|
|
101
|
+
'scannedPageCount', 'imageOnly', 'openingPages',
|
|
102
|
+
],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
};
|
|
105
|
+
const inspectOutputSchema = structuredClone(largeResultOutputSchema);
|
|
106
|
+
inspectOutputSchema.properties.identity = inspectIdentitySchema;
|
|
107
|
+
inspectOutputSchema.required.push('identity');
|
|
108
|
+
|
|
109
|
+
const definitions = new Map([
|
|
110
|
+
['pdf_inspect', {
|
|
111
|
+
description: 'Inspect one current PDF revision. Always retain the complete observation at output and return a bounded identity containing page and document metadata without traversing document content.',
|
|
112
|
+
outputSchema: inspectOutputSchema,
|
|
113
|
+
}],
|
|
114
|
+
['pdf_extract_tables', {
|
|
115
|
+
description: 'Extract tables from selected current PDF pages with deterministic published extraction. Set returnContent true to return complete bounded content; provide output to retain the complete immutable result. At least one result channel is required.',
|
|
116
|
+
outputSchema: largeResultOutputSchema,
|
|
117
|
+
}],
|
|
118
|
+
['pdf_find_table', {
|
|
119
|
+
description: 'Find a caller-named table in one current PDF without deciding its business role. Set returnContent true to return complete bounded content; provide output to retain the complete immutable result. At least one result channel is required.',
|
|
120
|
+
outputSchema: largeResultOutputSchema,
|
|
121
|
+
}],
|
|
122
|
+
['pdf_ocr', {
|
|
123
|
+
description: 'Observe selected current PDF pages through the fixed Aliyun qwen3.8-max OCR model and per-invocation Supen credential. Set returnContent true to return complete bounded content; provide output to retain the complete immutable result. At least one result channel is required.',
|
|
124
|
+
outputSchema: largeResultOutputSchema,
|
|
125
|
+
}],
|
|
126
|
+
['pdf_extract_table_details', {
|
|
127
|
+
description: 'Read visual table cells, text spans, fonts, colors, and line evidence from selected current PDF pages. Set returnContent true to return complete bounded content; provide output to retain the complete immutable result. At least one result channel is required.',
|
|
128
|
+
outputSchema: largeResultOutputSchema,
|
|
129
|
+
}],
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
const tools = manifest.tools.map((entry) => {
|
|
133
|
+
const definition = definitions.get(entry.name);
|
|
134
|
+
if (!definition) throw new Error(`pdf-provider-tool-definition-missing:${entry.name}`);
|
|
135
|
+
const contractPath = path.join(pdfRoot, entry.inputContract.path);
|
|
136
|
+
const bytes = readFileSync(contractPath);
|
|
137
|
+
if (createHash('sha256').update(bytes).digest('hex') !== entry.inputContract.sha256) {
|
|
138
|
+
throw new Error(`pdf-provider-input-contract-hash-invalid:${entry.name}`);
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
name: entry.name,
|
|
142
|
+
description: definition.description,
|
|
143
|
+
inputSchema: JSON.parse(bytes.toString('utf8')),
|
|
144
|
+
outputSchema: definition.outputSchema,
|
|
145
|
+
annotations: {
|
|
146
|
+
readOnlyHint: true,
|
|
147
|
+
idempotentHint: true,
|
|
148
|
+
destructiveHint: false,
|
|
149
|
+
openWorldHint: false,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
async function callTool(name, args) {
|
|
155
|
+
switch (name) {
|
|
156
|
+
case 'pdf_inspect': return createToolResult(await pdfInspect(args));
|
|
157
|
+
case 'pdf_extract_tables': return createToolResult(await pdfExtractTables(args));
|
|
158
|
+
case 'pdf_find_table': return createToolResult(await pdfFindTable(args));
|
|
159
|
+
case 'pdf_ocr': return createToolResult(await pdfOcr(args));
|
|
160
|
+
case 'pdf_extract_table_details': return createToolResult(await pdfExtractTableDetails(args));
|
|
161
|
+
default: throw Object.assign(new Error(`Unknown tool: ${name}`), { code: -32601 });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function pdfInspect(args) {
|
|
166
|
+
rejectUnexpectedArgs(args, ['input', 'returnContent', 'output']);
|
|
167
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
168
|
+
requireString(args.output, 'output');
|
|
169
|
+
const result = await runJsonCandidateChain(pdfCandidates, ['inspect', input, '--json']);
|
|
170
|
+
const payload = result.json;
|
|
171
|
+
const document = payload?.document ?? payload;
|
|
172
|
+
if (!document || !Number.isInteger(document.pages) || document.pages < 1) {
|
|
173
|
+
throw new Error('pdf-inspect-runtime-result-invalid');
|
|
174
|
+
}
|
|
175
|
+
const metadata = document.metadata && typeof document.metadata === 'object' ? document.metadata : {};
|
|
176
|
+
const identity = {
|
|
177
|
+
format: 'pdf',
|
|
178
|
+
pageCount: document.pages,
|
|
179
|
+
title: nullableText(metadata.title),
|
|
180
|
+
author: nullableText(metadata.author),
|
|
181
|
+
subject: nullableText(metadata.subject),
|
|
182
|
+
imageCount: nonnegativeInteger(document.image_count),
|
|
183
|
+
wordCount: nonnegativeInteger(document.word_count),
|
|
184
|
+
scannedPageCount: nonnegativeInteger(document.scanned_page_count),
|
|
185
|
+
imageOnly: document.image_only === true,
|
|
186
|
+
openingPages: (Array.isArray(document.page_sizes) ? document.page_sizes : []).slice(0, 3)
|
|
187
|
+
.map((page) => ({
|
|
188
|
+
page: positiveInteger(page.page),
|
|
189
|
+
width: positiveNumber(page.width),
|
|
190
|
+
height: positiveNumber(page.height),
|
|
191
|
+
imageCount: nonnegativeInteger(page.image_count),
|
|
192
|
+
wordCount: nonnegativeInteger(page.word_count),
|
|
193
|
+
imageOnly: page.image_only === true,
|
|
194
|
+
})),
|
|
195
|
+
};
|
|
196
|
+
return deliverLargeJsonResult({
|
|
197
|
+
tool: 'pdf_inspect', args, runtime: commandRuntime(result), payload, sourcePaths: [input], identity,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function pdfExtractTables(args) {
|
|
202
|
+
rejectUnexpectedArgs(args, ['input', 'pages', 'autoSpan', 'returnContent', 'output']);
|
|
203
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
204
|
+
const commandArgs = ['extract-tables', input];
|
|
205
|
+
appendPdfFlags(commandArgs, args);
|
|
206
|
+
commandArgs.push('--json');
|
|
207
|
+
const result = await runJsonCandidateChain(pdfCandidates, commandArgs);
|
|
208
|
+
return deliverLargeJsonResult({
|
|
209
|
+
tool: 'pdf_extract_tables', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input],
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function pdfFindTable(args) {
|
|
214
|
+
rejectUnexpectedArgs(args, ['input', 'name', 'autoSpan', 'returnContent', 'output']);
|
|
215
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
216
|
+
const commandArgs = ['find-table', input, requireString(args.name, 'name')];
|
|
217
|
+
appendPdfFlags(commandArgs, args);
|
|
218
|
+
commandArgs.push('--json');
|
|
219
|
+
const result = await runJsonCandidateChain(pdfCandidates, commandArgs);
|
|
220
|
+
return deliverLargeJsonResult({
|
|
221
|
+
tool: 'pdf_find_table', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function pdfExtractTableDetails(args) {
|
|
226
|
+
rejectUnexpectedArgs(args, ['input', 'pages', 'returnContent', 'output']);
|
|
227
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
228
|
+
const commandArgs = ['extract-table-details', input];
|
|
229
|
+
appendPages(commandArgs, args.pages);
|
|
230
|
+
commandArgs.push('--json');
|
|
231
|
+
const result = await runJsonCandidateChain(pdfCandidates, commandArgs);
|
|
232
|
+
return deliverLargeJsonResult({
|
|
233
|
+
tool: 'pdf_extract_table_details', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input],
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function pdfOcr(args) {
|
|
238
|
+
rejectUnexpectedArgs(args, ['input', 'output', 'pages', 'returnContent']);
|
|
239
|
+
const input = path.resolve(requireString(args.input, 'input'));
|
|
240
|
+
const commandArgs = ['ocr', input, '--provider', 'llm', '--llm-model', 'qwen3.8-max'];
|
|
241
|
+
appendPages(commandArgs, args.pages);
|
|
242
|
+
commandArgs.push('--json');
|
|
243
|
+
const result = await runJsonCandidateChain(pdfCandidates, commandArgs);
|
|
244
|
+
return deliverLargeJsonResult({
|
|
245
|
+
tool: 'pdf_ocr', args, runtime: commandRuntime(result), payload: result.json, sourcePaths: [input],
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function rejectUnexpectedArgs(args, allowed) {
|
|
250
|
+
const unexpected = Object.keys(args).filter(key => !allowed.includes(key));
|
|
251
|
+
if (unexpected.length > 0) {
|
|
252
|
+
throw Object.assign(new Error(`Unexpected arguments: ${unexpected.join(', ')}`), { code: -32602 });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function appendPdfFlags(commandArgs, args) {
|
|
257
|
+
appendPages(commandArgs, args.pages);
|
|
258
|
+
if (args.autoSpan === true) commandArgs.push('--auto-span');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function appendPages(commandArgs, pages) {
|
|
262
|
+
if (Array.isArray(pages) && pages.length > 0) commandArgs.push('--pages', pages.join(','));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function commandRuntime(result) {
|
|
266
|
+
return { command: result.command, arguments: result.args };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function nullableText(value) {
|
|
270
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function nonnegativeInteger(value) {
|
|
274
|
+
if (!Number.isInteger(value) || value < 0) throw new Error('pdf-inspect-runtime-result-invalid');
|
|
275
|
+
return value;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function positiveInteger(value) {
|
|
279
|
+
if (!Number.isInteger(value) || value < 1) throw new Error('pdf-inspect-runtime-result-invalid');
|
|
280
|
+
return value;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function positiveNumber(value) {
|
|
284
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
|
|
285
|
+
throw new Error('pdf-inspect-runtime-result-invalid');
|
|
286
|
+
}
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function readJson(file) {
|
|
291
|
+
return JSON.parse(readFileSync(file, 'utf8'));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const server = new McpStdioServer({
|
|
295
|
+
name: 'tiwater-pdf',
|
|
296
|
+
version: packageJson.version,
|
|
297
|
+
instructions: [
|
|
298
|
+
'Generic PDF inspection, deterministic table extraction, and OCR fixed to Aliyun qwen3.8-max through the per-invocation Supen Gateway credential.',
|
|
299
|
+
'Every output path is an immutable observation artifact identity: an identical request may replay identical bytes, while every different result uses a different path.',
|
|
300
|
+
'PDF tools report technical observations only and never assign business roles, source dispositions, or delivery decisions.',
|
|
301
|
+
].join(' '),
|
|
302
|
+
tools,
|
|
303
|
+
callTool,
|
|
304
|
+
logger: message => process.stderr.write(`${message}\n`),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
server.start();
|