@tiwater/office-mcp 0.2.0 → 0.4.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/tool-runtime.mjs +35 -2
- package/office/README.md +5 -1
- package/office/index.mjs +287 -296
- package/package.json +9 -2
- package/_shared/mcp-stdio.mjs +0 -152
package/_shared/tool-runtime.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
@@ -78,7 +79,7 @@ export function requireString(value, label) {
|
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
async function runCommand(candidate, args, options) {
|
|
81
|
-
const env = { ...process.env, ...(candidate.env || {}), ...(options.env || {}) };
|
|
82
|
+
const env = await withDotnetRoot({ ...process.env, ...(candidate.env || {}), ...(options.env || {}) });
|
|
82
83
|
const cwd = candidate.cwd || options.cwd || repoRoot;
|
|
83
84
|
const commandArgs = [...(candidate.argsPrefix || []), ...args];
|
|
84
85
|
|
|
@@ -99,10 +100,42 @@ async function runCommand(candidate, args, options) {
|
|
|
99
100
|
child.on('close', code => {
|
|
100
101
|
const allowedExitCodes = options.allowedExitCodes ?? [0];
|
|
101
102
|
if (allowedExitCodes.includes(code)) {
|
|
102
|
-
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs });
|
|
103
|
+
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs, cwd });
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
105
106
|
reject(new Error(`${candidate.command} ${commandArgs.join(' ')} failed with exit code ${code}\n${stderr || stdout}`));
|
|
106
107
|
});
|
|
107
108
|
});
|
|
108
109
|
}
|
|
110
|
+
|
|
111
|
+
async function withDotnetRoot(env) {
|
|
112
|
+
const architectureVariable = process.arch === 'arm64'
|
|
113
|
+
? 'DOTNET_ROOT_ARM64'
|
|
114
|
+
: process.arch === 'x64'
|
|
115
|
+
? 'DOTNET_ROOT_X64'
|
|
116
|
+
: null;
|
|
117
|
+
if (env.DOTNET_ROOT || (architectureVariable && env[architectureVariable])) return env;
|
|
118
|
+
|
|
119
|
+
const dotnet = await findOnPath(process.platform === 'win32' ? 'dotnet.exe' : 'dotnet', env.PATH);
|
|
120
|
+
if (!dotnet) return env;
|
|
121
|
+
|
|
122
|
+
const root = path.dirname(dotnet);
|
|
123
|
+
return {
|
|
124
|
+
...env,
|
|
125
|
+
DOTNET_ROOT: root,
|
|
126
|
+
...(architectureVariable ? { [architectureVariable]: root } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function findOnPath(command, pathValue) {
|
|
131
|
+
for (const directory of String(pathValue ?? '').split(path.delimiter).filter(Boolean)) {
|
|
132
|
+
const candidate = path.join(directory, command);
|
|
133
|
+
try {
|
|
134
|
+
await fs.access(candidate, fsConstants.X_OK);
|
|
135
|
+
return candidate;
|
|
136
|
+
} catch {
|
|
137
|
+
// Continue to the next PATH entry.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
package/office/README.md
CHANGED
|
@@ -15,7 +15,6 @@ Shared stdio MCP server for Office document workflows.
|
|
|
15
15
|
- `xlsx_export_json`
|
|
16
16
|
- `xlsx_validate`
|
|
17
17
|
- `pptx_inspect`
|
|
18
|
-
- `pptx_inspect_detail`
|
|
19
18
|
- `pptx_export_json`
|
|
20
19
|
|
|
21
20
|
## Run
|
|
@@ -26,3 +25,8 @@ the consumer, then run `tiwater-office-mcp` as a stdio MCP server.
|
|
|
26
25
|
The server invokes published `tiwater-docx`, `tiwater-xlsx`, and
|
|
27
26
|
`tiwater-pptx` commands from `PATH`. It does not require a source checkout or
|
|
28
27
|
fall back to local projects.
|
|
28
|
+
|
|
29
|
+
The official MCP SDK derives the schemas advertised to clients and validates
|
|
30
|
+
tool arguments and structured results before they cross the protocol boundary.
|
|
31
|
+
Large observations and exports are written to a caller-selected new JSON
|
|
32
|
+
artifact. MCP returns only the artifact path, hash, and byte count.
|
package/office/index.mjs
CHANGED
|
@@ -1,347 +1,312 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
4
|
import path from 'node:path';
|
|
3
5
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import {
|
|
6
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
7
|
+
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
8
|
+
import * as z from 'zod/v4';
|
|
5
9
|
import {
|
|
6
10
|
commandCandidate,
|
|
7
11
|
createToolResult,
|
|
8
|
-
maybeReadJson,
|
|
9
12
|
requireString,
|
|
10
|
-
resolveRepoPath,
|
|
11
|
-
runCandidateChain,
|
|
12
13
|
runJsonCandidateChain,
|
|
13
14
|
withTempJsonFile,
|
|
14
15
|
} from '../_shared/tool-runtime.mjs';
|
|
15
16
|
|
|
17
|
+
const packageMetadata = JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8'));
|
|
18
|
+
const invocationCwd = process.cwd();
|
|
19
|
+
|
|
16
20
|
const docxCandidates = [
|
|
17
|
-
commandCandidate('tiwater-docx'),
|
|
21
|
+
commandCandidate('tiwater-docx', [], { cwd: invocationCwd }),
|
|
18
22
|
];
|
|
19
23
|
|
|
20
24
|
const xlsxCandidates = [
|
|
21
|
-
commandCandidate('tiwater-xlsx'),
|
|
25
|
+
commandCandidate('tiwater-xlsx', [], { cwd: invocationCwd }),
|
|
22
26
|
];
|
|
23
27
|
|
|
24
28
|
const pptxCandidates = [
|
|
25
|
-
commandCandidate('tiwater-pptx'),
|
|
29
|
+
commandCandidate('tiwater-pptx', [], { cwd: invocationCwd }),
|
|
26
30
|
];
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
32
|
+
const pathInput = z.string().trim().min(1);
|
|
33
|
+
const migrationAction = z.enum([
|
|
34
|
+
'place-content',
|
|
35
|
+
'keep-template-content',
|
|
36
|
+
'keep-template-label',
|
|
37
|
+
'select-template-option',
|
|
38
|
+
'exclude-source',
|
|
39
|
+
'review-source',
|
|
40
|
+
]);
|
|
41
|
+
const targetActions = new Set([
|
|
42
|
+
'place-content',
|
|
43
|
+
'keep-template-content',
|
|
44
|
+
'keep-template-label',
|
|
45
|
+
'select-template-option',
|
|
46
|
+
]);
|
|
47
|
+
const terminalActions = new Set(['exclude-source', 'review-source']);
|
|
48
|
+
|
|
49
|
+
const migrationChoiceInput = z.object({
|
|
50
|
+
sourceChoiceId: z.string().trim().min(1),
|
|
51
|
+
action: migrationAction,
|
|
52
|
+
targetChoiceId: z.string().trim().min(1).optional(),
|
|
53
|
+
cardinality: z.enum(['one', 'all']).optional(),
|
|
54
|
+
}).strict().superRefine((choice, context) => {
|
|
55
|
+
if (targetActions.has(choice.action) && !choice.targetChoiceId) {
|
|
56
|
+
context.addIssue({ code: 'custom', path: ['targetChoiceId'], message: `${choice.action} requires targetChoiceId` });
|
|
57
|
+
}
|
|
58
|
+
if (terminalActions.has(choice.action) && choice.targetChoiceId) {
|
|
59
|
+
context.addIssue({ code: 'custom', path: ['targetChoiceId'], message: `${choice.action} forbids targetChoiceId` });
|
|
60
|
+
}
|
|
61
|
+
if (choice.cardinality === 'all' && !terminalActions.has(choice.action)) {
|
|
62
|
+
context.addIssue({ code: 'custom', path: ['cardinality'], message: 'cardinality all is limited to terminal actions' });
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const templateCleanupInput = z.object({
|
|
67
|
+
targetChoiceId: z.string().trim().min(1),
|
|
68
|
+
scope: z.enum(['cell', 'row']),
|
|
69
|
+
}).strict();
|
|
70
|
+
|
|
71
|
+
const templateMigrationInput = z.object({
|
|
72
|
+
source: pathInput.describe('Path to the current source DOCX.'),
|
|
73
|
+
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
74
|
+
output: pathInput.describe('Path to the migrated output DOCX.'),
|
|
75
|
+
receiptOutput: pathInput.describe('New JSON receipt artifact path. Existing files are never overwritten.'),
|
|
76
|
+
choices: z.array(migrationChoiceInput).describe('Exactly one business choice for every source id returned by docx_list_migration_choices.'),
|
|
77
|
+
templateCleanup: z.array(templateCleanupInput).optional().describe('Optional baseline-owned placeholders or example rows to clear.'),
|
|
78
|
+
}).strict();
|
|
79
|
+
|
|
80
|
+
const runtimeIdentity = z.object({
|
|
81
|
+
command: z.string(),
|
|
82
|
+
cwd: z.string(),
|
|
83
|
+
}).strict();
|
|
84
|
+
|
|
85
|
+
const migrationChoiceOutput = z.object({
|
|
86
|
+
id: z.string(),
|
|
87
|
+
kind: z.string(),
|
|
88
|
+
scope: z.string(),
|
|
89
|
+
text: z.string().nullable(),
|
|
90
|
+
count: z.number().int(),
|
|
91
|
+
requiredCardinality: z.string().nullable(),
|
|
92
|
+
context: z.record(z.string(), z.unknown()).nullable(),
|
|
93
|
+
allowedActions: z.array(z.string()),
|
|
94
|
+
}).strict();
|
|
95
|
+
|
|
96
|
+
const migrationCatalog = z.object({
|
|
97
|
+
schema: z.string(),
|
|
98
|
+
pass: z.boolean(),
|
|
99
|
+
sourceSha256: z.string(),
|
|
100
|
+
baselineSha256: z.string(),
|
|
101
|
+
sources: z.array(migrationChoiceOutput),
|
|
102
|
+
targets: z.array(migrationChoiceOutput),
|
|
103
|
+
}).strict();
|
|
104
|
+
|
|
105
|
+
const migrationReceipt = z.object({
|
|
106
|
+
schema: z.string(),
|
|
107
|
+
toolVersion: z.string(),
|
|
108
|
+
status: z.enum(['pass', 'review-required', 'failed']),
|
|
109
|
+
pass: z.boolean(),
|
|
110
|
+
reviewRequired: z.boolean(),
|
|
111
|
+
outputVerified: z.boolean(),
|
|
112
|
+
output: z.string().nullable(),
|
|
113
|
+
plan: z.string().nullable(),
|
|
114
|
+
failures: z.array(z.unknown()),
|
|
115
|
+
}).passthrough();
|
|
116
|
+
|
|
117
|
+
const inputOnly = z.object({ input: pathInput }).strict();
|
|
118
|
+
const artifactInput = z.object({
|
|
119
|
+
input: pathInput,
|
|
120
|
+
output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
|
|
121
|
+
}).strict();
|
|
122
|
+
const artifact = z.object({
|
|
123
|
+
path: z.string(),
|
|
124
|
+
sha256: z.string().regex(/^[0-9a-f]{64}$/),
|
|
125
|
+
bytes: z.number().int().nonnegative(),
|
|
126
|
+
}).strict();
|
|
127
|
+
|
|
128
|
+
const migrationCatalogOutput = z.object({
|
|
129
|
+
tool: z.literal('docx_list_migration_choices'),
|
|
130
|
+
runtime: runtimeIdentity,
|
|
131
|
+
artifact,
|
|
132
|
+
summary: z.object({
|
|
133
|
+
schema: z.string(),
|
|
134
|
+
pass: z.boolean(),
|
|
135
|
+
sourceSha256: z.string(),
|
|
136
|
+
baselineSha256: z.string(),
|
|
137
|
+
sourceCount: z.number().int().nonnegative(),
|
|
138
|
+
targetCount: z.number().int().nonnegative(),
|
|
139
|
+
}).strict(),
|
|
140
|
+
}).strict();
|
|
141
|
+
|
|
142
|
+
function migrationReceiptOutput(tool) {
|
|
143
|
+
return z.object({
|
|
144
|
+
tool: z.literal(tool),
|
|
145
|
+
runtime: runtimeIdentity,
|
|
146
|
+
artifact,
|
|
147
|
+
summary: z.object({
|
|
148
|
+
schema: z.string(),
|
|
149
|
+
toolVersion: z.string(),
|
|
150
|
+
status: z.enum(['pass', 'review-required', 'failed']),
|
|
151
|
+
pass: z.boolean(),
|
|
152
|
+
reviewRequired: z.boolean(),
|
|
153
|
+
outputVerified: z.boolean(),
|
|
154
|
+
output: z.string().nullable(),
|
|
155
|
+
plan: z.string().nullable(),
|
|
156
|
+
failureCount: z.number().int().nonnegative(),
|
|
157
|
+
}).strict(),
|
|
158
|
+
}).strict();
|
|
70
159
|
}
|
|
71
160
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
};
|
|
161
|
+
function artifactOutput(tool) {
|
|
162
|
+
return z.object({ tool: z.literal(tool), runtime: runtimeIdentity, artifact }).strict();
|
|
147
163
|
}
|
|
148
164
|
|
|
149
165
|
const tools = [
|
|
150
166
|
{
|
|
151
167
|
name: 'docx_inspect',
|
|
152
|
-
description: 'Inspect a DOCX document and
|
|
153
|
-
inputSchema:
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
},
|
|
168
|
+
description: 'Inspect a DOCX document and write one unified JSON observation containing placeholders, comments, anchors, tables, fields, flow, fonts, and formatting metrics.',
|
|
169
|
+
inputSchema: artifactInput,
|
|
170
|
+
outputSchema: artifactOutput('docx_inspect'),
|
|
171
|
+
handler: docxInspect,
|
|
167
172
|
},
|
|
168
173
|
{
|
|
169
174
|
name: 'docx_list_migration_choices',
|
|
170
|
-
description: '
|
|
171
|
-
inputSchema: {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
additionalProperties: false,
|
|
179
|
-
},
|
|
180
|
-
outputSchema: migrationCatalogOutputSchema,
|
|
175
|
+
description: 'Write every current source item that still needs a business choice and the selectable current baseline targets to a run-local JSON artifact. Returns only artifact metadata and counts; it does not recommend a choice.',
|
|
176
|
+
inputSchema: z.object({
|
|
177
|
+
source: pathInput.describe('Path to the current source DOCX.'),
|
|
178
|
+
baseline: pathInput.describe('Path to the selected current baseline DOCX.'),
|
|
179
|
+
output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
|
|
180
|
+
}).strict(),
|
|
181
|
+
outputSchema: migrationCatalogOutput,
|
|
182
|
+
handler: docxListMigrationChoices,
|
|
181
183
|
},
|
|
182
184
|
{
|
|
183
185
|
name: 'docx_migrate_template',
|
|
184
186
|
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:
|
|
186
|
-
outputSchema:
|
|
187
|
+
inputSchema: templateMigrationInput,
|
|
188
|
+
outputSchema: migrationReceiptOutput('docx_migrate_template'),
|
|
189
|
+
handler: docxMigrateTemplate,
|
|
187
190
|
},
|
|
188
191
|
{
|
|
189
192
|
name: 'docx_verify_migration',
|
|
190
193
|
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:
|
|
192
|
-
outputSchema:
|
|
194
|
+
inputSchema: templateMigrationInput,
|
|
195
|
+
outputSchema: migrationReceiptOutput('docx_verify_migration'),
|
|
196
|
+
handler: docxVerifyMigration,
|
|
193
197
|
},
|
|
194
198
|
{
|
|
195
199
|
name: 'docx_compare',
|
|
196
200
|
description: 'Compare two DOCX files and report package, metric, and style differences.',
|
|
197
|
-
inputSchema: {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
baseline: { type: 'string' },
|
|
201
|
-
updated: { type: 'string' },
|
|
202
|
-
},
|
|
203
|
-
required: ['baseline', 'updated'],
|
|
204
|
-
},
|
|
201
|
+
inputSchema: z.object({ baseline: pathInput, updated: pathInput }).strict(),
|
|
202
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
203
|
+
handler: docxCompare,
|
|
205
204
|
},
|
|
206
205
|
{
|
|
207
206
|
name: 'docx_validate_template_transform',
|
|
208
207
|
description: 'Validate whether a source DOCX template and target DOCX template are structurally compatible.',
|
|
209
|
-
inputSchema: {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
sourceTemplate: { type: 'string' },
|
|
213
|
-
targetTemplate: { type: 'string' },
|
|
214
|
-
},
|
|
215
|
-
required: ['sourceTemplate', 'targetTemplate'],
|
|
216
|
-
},
|
|
208
|
+
inputSchema: z.object({ sourceTemplate: pathInput, targetTemplate: pathInput }).strict(),
|
|
209
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
210
|
+
handler: docxValidateTemplateTransform,
|
|
217
211
|
},
|
|
218
212
|
{
|
|
219
213
|
name: 'docx_export_json',
|
|
220
|
-
description: 'Export
|
|
221
|
-
inputSchema:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
input: { type: 'string' },
|
|
225
|
-
output: { type: 'string' },
|
|
226
|
-
},
|
|
227
|
-
required: ['input'],
|
|
228
|
-
},
|
|
214
|
+
description: 'Export DOCX body content to a new JSON artifact without returning the full document through MCP.',
|
|
215
|
+
inputSchema: artifactInput,
|
|
216
|
+
outputSchema: artifactOutput('docx_export_json'),
|
|
217
|
+
handler: docxExportJson,
|
|
229
218
|
},
|
|
230
219
|
{
|
|
231
220
|
name: 'xlsx_inspect',
|
|
232
|
-
description: 'Inspect an XLSX workbook and
|
|
233
|
-
inputSchema:
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
required: ['input'],
|
|
237
|
-
},
|
|
221
|
+
description: 'Inspect an XLSX workbook and write one JSON observation containing workbook structure, exported values, formulas, styles, merged ranges, and conversion evidence.',
|
|
222
|
+
inputSchema: artifactInput,
|
|
223
|
+
outputSchema: artifactOutput('xlsx_inspect'),
|
|
224
|
+
handler: xlsxInspect,
|
|
238
225
|
},
|
|
239
226
|
{
|
|
240
227
|
name: 'xlsx_export_json',
|
|
241
228
|
description: 'Export workbook sheet data from XLSX as structured JSON.',
|
|
242
|
-
inputSchema: {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
required: ['input'],
|
|
250
|
-
},
|
|
229
|
+
inputSchema: z.object({
|
|
230
|
+
input: pathInput,
|
|
231
|
+
output: pathInput.describe('New JSON artifact path. Existing files are never overwritten.'),
|
|
232
|
+
resolveMergedCells: z.boolean().optional().describe('Resolve merged cells to project values.'),
|
|
233
|
+
}).strict(),
|
|
234
|
+
outputSchema: artifactOutput('xlsx_export_json'),
|
|
235
|
+
handler: xlsxExportJson,
|
|
251
236
|
},
|
|
252
237
|
{
|
|
253
238
|
name: 'xlsx_validate',
|
|
254
239
|
description: 'Validate an XLSX workbook package and return Open XML validation evidence.',
|
|
255
|
-
inputSchema:
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
required: ['input'],
|
|
259
|
-
},
|
|
240
|
+
inputSchema: inputOnly,
|
|
241
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
242
|
+
handler: xlsxValidate,
|
|
260
243
|
},
|
|
261
244
|
{
|
|
262
245
|
name: 'pptx_inspect',
|
|
263
|
-
description: 'Inspect a PPTX file and
|
|
264
|
-
inputSchema:
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
},
|
|
246
|
+
description: 'Inspect a PPTX file and write one detailed JSON observation containing slides, masters, layouts, shapes, transforms, paragraphs, runs, and placeholders.',
|
|
247
|
+
inputSchema: artifactInput,
|
|
248
|
+
outputSchema: artifactOutput('pptx_inspect'),
|
|
249
|
+
handler: pptxInspect,
|
|
278
250
|
},
|
|
279
251
|
{
|
|
280
252
|
name: 'pptx_export_json',
|
|
281
|
-
description: 'Export PPTX slide text and placeholder hints
|
|
282
|
-
inputSchema:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
input: { type: 'string' },
|
|
286
|
-
output: { type: 'string' },
|
|
287
|
-
},
|
|
288
|
-
required: ['input'],
|
|
289
|
-
},
|
|
253
|
+
description: 'Export PPTX slide text, notes, and placeholder hints to a new JSON artifact without returning the full presentation through MCP.',
|
|
254
|
+
inputSchema: artifactInput,
|
|
255
|
+
outputSchema: artifactOutput('pptx_export_json'),
|
|
256
|
+
handler: pptxExportJson,
|
|
290
257
|
},
|
|
291
258
|
];
|
|
292
259
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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 });
|
|
260
|
+
function buildServer() {
|
|
261
|
+
const server = new McpServer(
|
|
262
|
+
{ name: 'tiwater-office', version: packageMetadata.version },
|
|
263
|
+
{
|
|
264
|
+
instructions: 'Use the Office tools for technical document observation. For template migration, list the current choices, select only allowed business actions, migrate once, and independently verify the result. Never invent document values, identities, coordinates, plans, or edit operations.',
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
for (const tool of tools) {
|
|
268
|
+
server.registerTool(
|
|
269
|
+
tool.name,
|
|
270
|
+
{
|
|
271
|
+
description: tool.description,
|
|
272
|
+
inputSchema: tool.inputSchema,
|
|
273
|
+
...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),
|
|
274
|
+
...(tool.annotations ? { annotations: tool.annotations } : {}),
|
|
275
|
+
},
|
|
276
|
+
async args => createToolResult(await tool.handler(args)),
|
|
277
|
+
);
|
|
325
278
|
}
|
|
279
|
+
return server;
|
|
326
280
|
}
|
|
327
281
|
|
|
328
282
|
async function docxInspect(args) {
|
|
329
283
|
const input = requireString(args.input, 'input');
|
|
330
284
|
const result = await runJsonCandidateChain(docxCandidates, ['inspect', input, '--json']);
|
|
331
|
-
return {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const result = await runJsonCandidateChain(docxCandidates, ['inspect-tables', input, '--json']);
|
|
337
|
-
return { tool: 'docx_inspect_tables', runtime: commandRuntime(result), report: result.json };
|
|
285
|
+
return {
|
|
286
|
+
tool: 'docx_inspect',
|
|
287
|
+
runtime: commandRuntime(result),
|
|
288
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
289
|
+
};
|
|
338
290
|
}
|
|
339
291
|
|
|
340
292
|
async function docxListMigrationChoices(args) {
|
|
341
293
|
const source = requireString(args.source, 'source');
|
|
342
294
|
const baseline = requireString(args.baseline, 'baseline');
|
|
343
295
|
const result = await runJsonCandidateChain(docxCandidates, ['list-template-migration-choices', source, baseline]);
|
|
344
|
-
|
|
296
|
+
const catalog = migrationCatalog.parse(result.json);
|
|
297
|
+
return {
|
|
298
|
+
tool: 'docx_list_migration_choices',
|
|
299
|
+
runtime: commandRuntime(result),
|
|
300
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), catalog),
|
|
301
|
+
summary: {
|
|
302
|
+
schema: catalog.schema,
|
|
303
|
+
pass: catalog.pass,
|
|
304
|
+
sourceSha256: catalog.sourceSha256,
|
|
305
|
+
baselineSha256: catalog.baselineSha256,
|
|
306
|
+
sourceCount: catalog.sources.length,
|
|
307
|
+
targetCount: catalog.targets.length,
|
|
308
|
+
},
|
|
309
|
+
};
|
|
345
310
|
}
|
|
346
311
|
|
|
347
312
|
async function docxMigrateTemplate(args) {
|
|
@@ -369,7 +334,23 @@ async function runTemplateMigrationCommand(tool, command, args) {
|
|
|
369
334
|
docxCandidates,
|
|
370
335
|
[command, source, baseline, choicesPath, output],
|
|
371
336
|
{ allowedExitCodes: [0, 1] });
|
|
372
|
-
|
|
337
|
+
const receipt = migrationReceipt.parse(result.json);
|
|
338
|
+
return {
|
|
339
|
+
tool,
|
|
340
|
+
runtime: commandRuntime(result),
|
|
341
|
+
artifact: await writeJsonArtifact(requireString(args.receiptOutput, 'receiptOutput'), receipt),
|
|
342
|
+
summary: {
|
|
343
|
+
schema: receipt.schema,
|
|
344
|
+
toolVersion: receipt.toolVersion,
|
|
345
|
+
status: receipt.status,
|
|
346
|
+
pass: receipt.pass,
|
|
347
|
+
reviewRequired: receipt.reviewRequired,
|
|
348
|
+
outputVerified: receipt.outputVerified,
|
|
349
|
+
output: receipt.output,
|
|
350
|
+
plan: receipt.plan,
|
|
351
|
+
failureCount: receipt.failures.length,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
373
354
|
});
|
|
374
355
|
}
|
|
375
356
|
|
|
@@ -389,19 +370,22 @@ async function docxValidateTemplateTransform(args) {
|
|
|
389
370
|
|
|
390
371
|
async function docxExportJson(args) {
|
|
391
372
|
const input = requireString(args.input, 'input');
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
return { tool: 'docx_export_json', runtime: commandRuntime(result), document: JSON.parse(result.stdout) };
|
|
373
|
+
const result = await runJsonCandidateChain(docxCandidates, ['export-json', input]);
|
|
374
|
+
return {
|
|
375
|
+
tool: 'docx_export_json',
|
|
376
|
+
runtime: commandRuntime(result),
|
|
377
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
378
|
+
};
|
|
399
379
|
}
|
|
400
380
|
|
|
401
381
|
async function xlsxInspect(args) {
|
|
402
382
|
const input = requireString(args.input, 'input');
|
|
403
383
|
const result = await runJsonCandidateChain(xlsxCandidates, ['inspect', input, '--json']);
|
|
404
|
-
return {
|
|
384
|
+
return {
|
|
385
|
+
tool: 'xlsx_inspect',
|
|
386
|
+
runtime: commandRuntime(result),
|
|
387
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
388
|
+
};
|
|
405
389
|
}
|
|
406
390
|
|
|
407
391
|
async function xlsxExportJson(args) {
|
|
@@ -410,14 +394,12 @@ async function xlsxExportJson(args) {
|
|
|
410
394
|
if (args.resolveMergedCells) {
|
|
411
395
|
cmdArgs.push('--resolve-merged-cells');
|
|
412
396
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
const result = await runCandidateChain(xlsxCandidates, cmdArgs);
|
|
420
|
-
return { tool: 'xlsx_export_json', runtime: commandRuntime(result), workbook: JSON.parse(result.stdout) };
|
|
397
|
+
const result = await runJsonCandidateChain(xlsxCandidates, cmdArgs);
|
|
398
|
+
return {
|
|
399
|
+
tool: 'xlsx_export_json',
|
|
400
|
+
runtime: commandRuntime(result),
|
|
401
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
402
|
+
};
|
|
421
403
|
}
|
|
422
404
|
|
|
423
405
|
async function xlsxValidate(args) {
|
|
@@ -429,24 +411,33 @@ async function xlsxValidate(args) {
|
|
|
429
411
|
async function pptxInspect(args) {
|
|
430
412
|
const input = requireString(args.input, 'input');
|
|
431
413
|
const result = await runJsonCandidateChain(pptxCandidates, ['inspect', input, '--json']);
|
|
432
|
-
return {
|
|
414
|
+
return {
|
|
415
|
+
tool: 'pptx_inspect',
|
|
416
|
+
runtime: commandRuntime(result),
|
|
417
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
418
|
+
};
|
|
433
419
|
}
|
|
434
420
|
|
|
435
|
-
async function
|
|
421
|
+
async function pptxExportJson(args) {
|
|
436
422
|
const input = requireString(args.input, 'input');
|
|
437
|
-
const result = await runJsonCandidateChain(pptxCandidates, ['
|
|
438
|
-
return {
|
|
423
|
+
const result = await runJsonCandidateChain(pptxCandidates, ['export-json', input]);
|
|
424
|
+
return {
|
|
425
|
+
tool: 'pptx_export_json',
|
|
426
|
+
runtime: commandRuntime(result),
|
|
427
|
+
artifact: await writeJsonArtifact(requireString(args.output, 'output'), result.json),
|
|
428
|
+
};
|
|
439
429
|
}
|
|
440
430
|
|
|
441
|
-
async function
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
431
|
+
async function writeJsonArtifact(output, payload) {
|
|
432
|
+
const fullPath = path.resolve(output);
|
|
433
|
+
await mkdir(path.dirname(fullPath), { recursive: true });
|
|
434
|
+
const bytes = Buffer.from(`${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
435
|
+
await writeFile(fullPath, bytes, { flag: 'wx' });
|
|
436
|
+
return {
|
|
437
|
+
path: fullPath,
|
|
438
|
+
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
439
|
+
bytes: bytes.length,
|
|
440
|
+
};
|
|
450
441
|
}
|
|
451
442
|
|
|
452
443
|
function commandRuntime(result) {
|
|
@@ -456,7 +447,7 @@ function commandRuntime(result) {
|
|
|
456
447
|
};
|
|
457
448
|
}
|
|
458
449
|
|
|
459
|
-
|
|
450
|
+
serveStdio(buildServer);
|
|
460
451
|
|
|
461
452
|
async function runXlsxValidateCandidateChain(args) {
|
|
462
453
|
const errors = [];
|
|
@@ -487,7 +478,7 @@ async function runXlsxValidateCandidateChain(args) {
|
|
|
487
478
|
|
|
488
479
|
async function runValidationCommand(candidate, args) {
|
|
489
480
|
const env = { ...process.env, ...(candidate.env || {}) };
|
|
490
|
-
const cwd = candidate.cwd ||
|
|
481
|
+
const cwd = candidate.cwd || process.cwd();
|
|
491
482
|
const commandArgs = [...(candidate.argsPrefix || []), ...args];
|
|
492
483
|
|
|
493
484
|
return await new Promise((resolve, reject) => {
|
|
@@ -506,7 +497,7 @@ async function runValidationCommand(candidate, args) {
|
|
|
506
497
|
child.on('error', reject);
|
|
507
498
|
child.on('close', code => {
|
|
508
499
|
if (code === 0 || code === 1) {
|
|
509
|
-
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs });
|
|
500
|
+
resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs, cwd });
|
|
510
501
|
return;
|
|
511
502
|
}
|
|
512
503
|
reject(new Error(`${candidate.command} ${commandArgs.join(' ')} failed with exit code ${code}\n${stderr || stdout}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiwater/office-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Published MCP server for Tiwater Office document capabilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
"bin": {
|
|
13
13
|
"tiwater-office-mcp": "office/index.mjs"
|
|
14
14
|
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
15
18
|
"files": [
|
|
16
|
-
"_shared
|
|
19
|
+
"_shared/tool-runtime.mjs",
|
|
17
20
|
"office/index.mjs",
|
|
18
21
|
"office/README.md"
|
|
19
22
|
],
|
|
@@ -22,5 +25,9 @@
|
|
|
22
25
|
},
|
|
23
26
|
"publishConfig": {
|
|
24
27
|
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/server": "2.0.0",
|
|
31
|
+
"zod": "4.4.3"
|
|
25
32
|
}
|
|
26
33
|
}
|
package/_shared/mcp-stdio.mjs
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
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
|
-
}
|