@levnikolaevich/hex-line-mcp 1.3.3 → 1.3.4
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/dist/hook.mjs +428 -0
- package/dist/server.mjs +6615 -0
- package/package.json +6 -8
- package/benchmark/atomic.mjs +0 -502
- package/benchmark/graph.mjs +0 -80
- package/benchmark/index.mjs +0 -144
- package/benchmark/workflows.mjs +0 -350
- package/hook.mjs +0 -466
- package/lib/benchmark-helpers.mjs +0 -541
- package/lib/bulk-replace.mjs +0 -65
- package/lib/changes.mjs +0 -176
- package/lib/coerce.mjs +0 -1
- package/lib/edit.mjs +0 -534
- package/lib/format.mjs +0 -138
- package/lib/graph-enrich.mjs +0 -226
- package/lib/hash.mjs +0 -1
- package/lib/info.mjs +0 -91
- package/lib/normalize.mjs +0 -1
- package/lib/outline.mjs +0 -145
- package/lib/read.mjs +0 -138
- package/lib/revisions.mjs +0 -238
- package/lib/search.mjs +0 -268
- package/lib/security.mjs +0 -112
- package/lib/setup.mjs +0 -275
- package/lib/tree.mjs +0 -236
- package/lib/update-check.mjs +0 -1
- package/lib/verify.mjs +0 -70
- package/server.mjs +0 -375
package/server.mjs
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* hex-line-mcp — MCP server for hash-verified file operations.
|
|
4
|
-
*
|
|
5
|
-
* 11 tools: read_file, edit_file, write_file, grep_search, outline, verify, directory_tree, get_file_info, setup_hooks, changes, bulk_replace
|
|
6
|
-
* FNV-1a 2-char tags + range checksums
|
|
7
|
-
* Security: root policy, path validation, binary/size rejection
|
|
8
|
-
* Transport: stdio
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { writeFileSync, mkdirSync } from "node:fs";
|
|
12
|
-
import { dirname } from "node:path";
|
|
13
|
-
import { createRequire } from "node:module";
|
|
14
|
-
const { version } = createRequire(import.meta.url)("./package.json");
|
|
15
|
-
import { z } from "zod";
|
|
16
|
-
import { createServerRuntime } from "@levnikolaevich/hex-common/runtime/mcp-bootstrap";
|
|
17
|
-
import { flexBool, flexNum } from "@levnikolaevich/hex-common/runtime/schema";
|
|
18
|
-
import { coerceParams } from "@levnikolaevich/hex-common/runtime/coerce";
|
|
19
|
-
import { checkForUpdates } from "@levnikolaevich/hex-common/runtime/update-check";
|
|
20
|
-
// LLM clients may send booleans as strings ("true"/"false").
|
|
21
|
-
// z.coerce.boolean() is unsafe: Boolean("false") === true.
|
|
22
|
-
// LLM clients may send numbers as strings ("5" instead of 5).
|
|
23
|
-
// z.coerce.number() generates {"type":"number"} → strict MCP clients reject strings.
|
|
24
|
-
// flexNum generates schema accepting both, coerces at runtime.
|
|
25
|
-
// Outer .optional() ensures JSON Schema marks field as not-required.
|
|
26
|
-
|
|
27
|
-
import { readFile } from "./lib/read.mjs";
|
|
28
|
-
import { editFile } from "./lib/edit.mjs";
|
|
29
|
-
import { grepSearch } from "./lib/search.mjs";
|
|
30
|
-
import { fileOutline } from "./lib/outline.mjs";
|
|
31
|
-
import { verifyChecksums } from "./lib/verify.mjs";
|
|
32
|
-
import { validateWritePath } from "./lib/security.mjs";
|
|
33
|
-
import { directoryTree } from "./lib/tree.mjs";
|
|
34
|
-
import { fileInfo } from "./lib/info.mjs";
|
|
35
|
-
import { setupHooks } from "./lib/setup.mjs";
|
|
36
|
-
import { fileChanges } from "./lib/changes.mjs";
|
|
37
|
-
import { bulkReplace } from "./lib/bulk-replace.mjs";
|
|
38
|
-
|
|
39
|
-
const { server, StdioServerTransport } = await createServerRuntime({
|
|
40
|
-
name: "hex-line-mcp",
|
|
41
|
-
version,
|
|
42
|
-
installDir: "mcp/hex-line-mcp",
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// ==================== read_file ====================
|
|
47
|
-
|
|
48
|
-
server.registerTool("read_file", {
|
|
49
|
-
title: "Read File",
|
|
50
|
-
description:
|
|
51
|
-
"Read a file with hash-annotated lines, range checksums, and current revision. " +
|
|
52
|
-
"Use offset/limit for targeted reads; use outline first for large code files.",
|
|
53
|
-
inputSchema: z.object({
|
|
54
|
-
path: z.string().optional().describe("File or directory path"),
|
|
55
|
-
paths: z.array(z.string()).optional().describe("Array of file paths to read (batch mode)"),
|
|
56
|
-
offset: flexNum().describe("Start line (1-indexed, default: 1)"),
|
|
57
|
-
limit: flexNum().describe("Max lines (default: 2000, 0 = all)"),
|
|
58
|
-
plain: flexBool().describe("Omit hashes (lineNum|content)"),
|
|
59
|
-
}),
|
|
60
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
61
|
-
}, async (rawParams) => {
|
|
62
|
-
const { path: p, paths: multi, offset, limit, plain } = coerceParams(rawParams);
|
|
63
|
-
try {
|
|
64
|
-
if (multi && multi.length > 0 && !p) {
|
|
65
|
-
const results = [];
|
|
66
|
-
for (const fp of multi) {
|
|
67
|
-
try {
|
|
68
|
-
results.push(readFile(fp, { offset, limit, plain }));
|
|
69
|
-
} catch (e) {
|
|
70
|
-
results.push(`File: ${fp}\n\nERROR: ${e.message}`);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return { content: [{ type: "text", text: results.join("\n\n---\n\n") }] };
|
|
74
|
-
}
|
|
75
|
-
if (!p) throw new Error("Either 'path' or 'paths' is required");
|
|
76
|
-
return { content: [{ type: "text", text: readFile(p, { offset, limit, plain }) }] };
|
|
77
|
-
} catch (e) {
|
|
78
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// ==================== edit_file ====================
|
|
84
|
-
|
|
85
|
-
server.registerTool("edit_file", {
|
|
86
|
-
title: "Edit File",
|
|
87
|
-
description:
|
|
88
|
-
"Apply revision-aware partial edits to one file. " +
|
|
89
|
-
"Prefer one batched call per file. Supports set_line, replace_lines, insert_after, and replace_between. " +
|
|
90
|
-
"For text rename/refactor use bulk_replace.",
|
|
91
|
-
inputSchema: z.object({
|
|
92
|
-
path: z.string().describe("File to edit"),
|
|
93
|
-
edits: z.string().describe(
|
|
94
|
-
'JSON array. Examples:\n' +
|
|
95
|
-
'{"set_line":{"anchor":"ab.12","new_text":"new"}} — replace line\n' +
|
|
96
|
-
'{"replace_lines":{"start_anchor":"ab.10","end_anchor":"cd.15","new_text":"...","range_checksum":"10-15:a1b2c3d4"}} — range\n' +
|
|
97
|
-
'{"replace_between":{"start_anchor":"ab.10","end_anchor":"cd.40","new_text":"...","boundary_mode":"inclusive"}} — block rewrite\n' +
|
|
98
|
-
'{"insert_after":{"anchor":"ab.20","text":"inserted"}} — insert below. For text rename use bulk_replace tool.',
|
|
99
|
-
),
|
|
100
|
-
dry_run: flexBool().describe("Preview changes without writing"),
|
|
101
|
-
restore_indent: flexBool().describe("Auto-fix indentation to match anchor (default: false)"),
|
|
102
|
-
base_revision: z.string().optional().describe("Prior revision from read_file/edit_file. Enables conservative auto-rebase for same-file follow-up edits."),
|
|
103
|
-
conflict_policy: z.enum(["strict", "conservative"]).optional().describe('Conflict handling (default: "conservative"). "conservative" returns structured CONFLICT output for stale edits instead of forcing reread.'),
|
|
104
|
-
}),
|
|
105
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
106
|
-
}, async (rawParams) => {
|
|
107
|
-
const { path: p, edits: json, dry_run, restore_indent, base_revision, conflict_policy } = coerceParams(rawParams);
|
|
108
|
-
try {
|
|
109
|
-
const parsed = JSON.parse(json);
|
|
110
|
-
if (!Array.isArray(parsed) || !parsed.length) throw new Error("Edits: non-empty JSON array required");
|
|
111
|
-
return {
|
|
112
|
-
content: [{
|
|
113
|
-
type: "text",
|
|
114
|
-
text: editFile(p, parsed, {
|
|
115
|
-
dryRun: dry_run,
|
|
116
|
-
restoreIndent: restore_indent,
|
|
117
|
-
baseRevision: base_revision,
|
|
118
|
-
conflictPolicy: conflict_policy,
|
|
119
|
-
}),
|
|
120
|
-
}],
|
|
121
|
-
};
|
|
122
|
-
} catch (e) {
|
|
123
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
// ==================== write_file ====================
|
|
129
|
-
|
|
130
|
-
server.registerTool("write_file", {
|
|
131
|
-
title: "Write File",
|
|
132
|
-
description:
|
|
133
|
-
"Create a new file or overwrite existing. Creates parent dirs. " +
|
|
134
|
-
"For existing files prefer edit_file (shows diff, verifies hashes).",
|
|
135
|
-
inputSchema: z.object({
|
|
136
|
-
path: z.string().describe("File path"),
|
|
137
|
-
content: z.string().describe("File content"),
|
|
138
|
-
}),
|
|
139
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
140
|
-
}, async (rawParams) => {
|
|
141
|
-
const { path: p, content } = coerceParams(rawParams);
|
|
142
|
-
try {
|
|
143
|
-
const abs = validateWritePath(p);
|
|
144
|
-
mkdirSync(dirname(abs), { recursive: true });
|
|
145
|
-
writeFileSync(abs, content, "utf-8");
|
|
146
|
-
return { content: [{ type: "text", text: `Created ${p} (${content.split("\n").length} lines)` }] };
|
|
147
|
-
} catch (e) {
|
|
148
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// ==================== grep_search ====================
|
|
154
|
-
|
|
155
|
-
server.registerTool("grep_search", {
|
|
156
|
-
title: "Search Files",
|
|
157
|
-
description:
|
|
158
|
-
"Search file contents with ripgrep. Returns hash-annotated matches with per-group checksums for direct editing. " +
|
|
159
|
-
"Output modes: content (default, edit-ready hashes+checksums), files (paths only), count (match counts). " +
|
|
160
|
-
"For single-line edits: grep -> set_line directly. For range edits: use checksum from grep output. " +
|
|
161
|
-
"ALWAYS prefer over shell grep/rg/findstr.",
|
|
162
|
-
inputSchema: z.object({
|
|
163
|
-
pattern: z.string().describe("Search pattern (regex by default, literal if literal:true)"),
|
|
164
|
-
path: z.string().optional().describe("Search dir/file (default: cwd)"),
|
|
165
|
-
glob: z.string().optional().describe('Glob filter (e.g. "*.ts")'),
|
|
166
|
-
type: z.string().optional().describe('File type (e.g. "js", "py")'),
|
|
167
|
-
output: z.enum(["content", "files", "count"]).optional().describe('Output format (default: content)'),
|
|
168
|
-
case_insensitive: flexBool().describe("Ignore case (-i)"),
|
|
169
|
-
smart_case: flexBool().describe("CI when pattern is all lowercase, CS if uppercase (-S)"),
|
|
170
|
-
literal: flexBool().describe("Literal string search, no regex (-F)"),
|
|
171
|
-
multiline: flexBool().describe("Pattern can span multiple lines (-U)"),
|
|
172
|
-
context: flexNum().describe("Symmetric context lines around matches (-C)"),
|
|
173
|
-
context_before: flexNum().describe("Context lines BEFORE match (-B)"),
|
|
174
|
-
context_after: flexNum().describe("Context lines AFTER match (-A)"),
|
|
175
|
-
limit: flexNum().describe("Max matches per file (default: 100)"),
|
|
176
|
-
total_limit: flexNum().describe("Total match events across all files; multiline matches count as 1 (0 = unlimited)"),
|
|
177
|
-
plain: flexBool().describe("Omit hash tags, return file:line:content"),
|
|
178
|
-
}),
|
|
179
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
180
|
-
}, async (rawParams) => {
|
|
181
|
-
const { pattern, path: p, glob, type, output, case_insensitive, smart_case, literal, multiline,
|
|
182
|
-
context, context_before, context_after, limit, total_limit, plain } = coerceParams(rawParams);
|
|
183
|
-
try {
|
|
184
|
-
const result = await grepSearch(pattern, {
|
|
185
|
-
path: p, glob, type, output, caseInsensitive: case_insensitive, smartCase: smart_case,
|
|
186
|
-
literal, multiline, context, contextBefore: context_before, contextAfter: context_after,
|
|
187
|
-
limit, totalLimit: total_limit, plain,
|
|
188
|
-
});
|
|
189
|
-
return { content: [{ type: "text", text: result }] };
|
|
190
|
-
} catch (e) {
|
|
191
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
// ==================== outline ====================
|
|
197
|
-
|
|
198
|
-
server.registerTool("outline", {
|
|
199
|
-
title: "File Outline",
|
|
200
|
-
description:
|
|
201
|
-
"AST-based structural outline: functions, classes, interfaces with line ranges. " +
|
|
202
|
-
"10-20 lines instead of 500 — 95% token reduction. " +
|
|
203
|
-
"Use before reading large code files. NOT for .md/.json/.yaml — use read_file.",
|
|
204
|
-
inputSchema: z.object({
|
|
205
|
-
path: z.string().describe("Source file path"),
|
|
206
|
-
}),
|
|
207
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
208
|
-
}, async (rawParams) => {
|
|
209
|
-
const { path: p } = coerceParams(rawParams);
|
|
210
|
-
try {
|
|
211
|
-
const result = await fileOutline(p);
|
|
212
|
-
return { content: [{ type: "text", text: result }] };
|
|
213
|
-
} catch (e) {
|
|
214
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
// ==================== verify ====================
|
|
220
|
-
|
|
221
|
-
server.registerTool("verify", {
|
|
222
|
-
title: "Verify Checksums",
|
|
223
|
-
description:
|
|
224
|
-
"Check whether held checksums and optional base_revision are still current, without rereading the file.",
|
|
225
|
-
inputSchema: z.object({
|
|
226
|
-
path: z.string().describe("File path"),
|
|
227
|
-
checksums: z.string().describe('JSON array of checksum strings, e.g. ["1-50:f7e2a1b0", "51-100:abcd1234"]'),
|
|
228
|
-
base_revision: z.string().optional().describe("Optional prior revision to compare against latest state."),
|
|
229
|
-
}),
|
|
230
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
231
|
-
}, async (rawParams) => {
|
|
232
|
-
const { path: p, checksums, base_revision } = coerceParams(rawParams);
|
|
233
|
-
try {
|
|
234
|
-
const parsed = JSON.parse(checksums);
|
|
235
|
-
if (!Array.isArray(parsed)) throw new Error("checksums must be a JSON array of strings");
|
|
236
|
-
return { content: [{ type: "text", text: verifyChecksums(p, parsed, { baseRevision: base_revision }) }] };
|
|
237
|
-
} catch (e) {
|
|
238
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// ==================== directory_tree ====================
|
|
244
|
-
|
|
245
|
-
server.registerTool("directory_tree", {
|
|
246
|
-
title: "Directory Tree",
|
|
247
|
-
description:
|
|
248
|
-
"Compact directory tree with root .gitignore support (path-based rules, negation). " +
|
|
249
|
-
"Supports pattern glob to find files/dirs by name (like find -name). " +
|
|
250
|
-
"Use to understand repo structure or find specific files/dirs. " +
|
|
251
|
-
"Skips node_modules, .git, dist by default.",
|
|
252
|
-
inputSchema: z.object({
|
|
253
|
-
path: z.string().describe("Directory path"),
|
|
254
|
-
pattern: z.string().optional().describe('Glob filter on names (e.g. "*-mcp", "*.mjs"). Returns flat match list instead of tree'),
|
|
255
|
-
type: z.enum(["file", "dir", "all"]).optional().describe('"file", "dir", or "all" (default). Like find -type f/d'),
|
|
256
|
-
max_depth: flexNum().describe("Max recursion depth (default: 3, or 20 in pattern mode)"),
|
|
257
|
-
gitignore: flexBool().describe("Respect root .gitignore patterns (default: true). Nested .gitignore not supported"),
|
|
258
|
-
format: z.enum(["compact", "full"]).optional().describe('"compact" = names only, no sizes, depth 1. "full" = default with sizes'),
|
|
259
|
-
}),
|
|
260
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
261
|
-
}, async (rawParams) => {
|
|
262
|
-
const { path: p, max_depth, gitignore, format, pattern, type: entryType } = coerceParams(rawParams);
|
|
263
|
-
try {
|
|
264
|
-
return { content: [{ type: "text", text: directoryTree(p, { max_depth, gitignore, format, pattern, type: entryType }) }] };
|
|
265
|
-
} catch (e) {
|
|
266
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
// ==================== get_file_info ====================
|
|
272
|
-
|
|
273
|
-
server.registerTool("get_file_info", {
|
|
274
|
-
title: "File Info",
|
|
275
|
-
description:
|
|
276
|
-
"File metadata without reading content: size, line count, modification time, type, binary detection. " +
|
|
277
|
-
"Use before reading large files to check size.",
|
|
278
|
-
inputSchema: z.object({
|
|
279
|
-
path: z.string().describe("File path"),
|
|
280
|
-
}),
|
|
281
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
282
|
-
}, async (rawParams) => {
|
|
283
|
-
const { path: p } = coerceParams(rawParams);
|
|
284
|
-
try {
|
|
285
|
-
return { content: [{ type: "text", text: fileInfo(p) }] };
|
|
286
|
-
} catch (e) {
|
|
287
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// ==================== setup_hooks ====================
|
|
293
|
-
|
|
294
|
-
server.registerTool("setup_hooks", {
|
|
295
|
-
title: "Setup Hooks",
|
|
296
|
-
description:
|
|
297
|
-
"Install or uninstall hex-line hooks in CLI agent settings. " +
|
|
298
|
-
"install: writes hooks to ~/.claude/settings.json, removes old per-project hooks. " +
|
|
299
|
-
"uninstall: removes hex-line hooks from global settings. " +
|
|
300
|
-
"Idempotent: re-running produces no changes if already in desired state.",
|
|
301
|
-
inputSchema: z.object({
|
|
302
|
-
agent: z.string().optional().describe('Target agent: "claude", "gemini", "codex", or "all" (default: "all")'),
|
|
303
|
-
action: z.string().optional().describe('"install" (default) or "uninstall"'),
|
|
304
|
-
}),
|
|
305
|
-
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
306
|
-
}, async (rawParams) => {
|
|
307
|
-
const { agent, action } = coerceParams(rawParams);
|
|
308
|
-
try {
|
|
309
|
-
return { content: [{ type: "text", text: setupHooks(agent, action) }] };
|
|
310
|
-
} catch (e) {
|
|
311
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// ==================== changes ====================
|
|
317
|
-
|
|
318
|
-
server.registerTool("changes", {
|
|
319
|
-
title: "Semantic Diff",
|
|
320
|
-
description:
|
|
321
|
-
"Compare file or directory against git ref (default: HEAD). For files: shows added/removed/modified symbols at AST level. " +
|
|
322
|
-
"For directories: lists changed files with insertions/deletions stats. Use to understand what changed before committing.",
|
|
323
|
-
inputSchema: z.object({
|
|
324
|
-
path: z.string().describe("File or directory path"),
|
|
325
|
-
compare_against: z.string().optional().describe('Git ref to compare against (default: "HEAD")'),
|
|
326
|
-
}),
|
|
327
|
-
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
328
|
-
}, async (rawParams) => {
|
|
329
|
-
const { path: p, compare_against } = coerceParams(rawParams);
|
|
330
|
-
try {
|
|
331
|
-
return { content: [{ type: "text", text: await fileChanges(p, compare_against) }] };
|
|
332
|
-
} catch (e) {
|
|
333
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// ==================== bulk_replace ====================
|
|
339
|
-
|
|
340
|
-
server.registerTool("bulk_replace", {
|
|
341
|
-
title: "Bulk Replace",
|
|
342
|
-
description:
|
|
343
|
-
"Search-and-replace across multiple files. Finds files by glob, applies ordered text replacements, returns per-file diffs. " +
|
|
344
|
-
"Use dry_run:true to preview. For single-file rename, set glob to the filename.",
|
|
345
|
-
inputSchema: z.object({
|
|
346
|
-
replacements: z.string().describe('JSON array of {old, new} pairs: [{"old":"foo","new":"bar"}]'),
|
|
347
|
-
glob: z.string().optional().describe('File glob (default: "**/*.{md,mjs,json,yml,ts,js}")'),
|
|
348
|
-
path: z.string().optional().describe("Root directory (default: cwd)"),
|
|
349
|
-
dry_run: flexBool().describe("Preview without writing (default: false)"),
|
|
350
|
-
max_files: flexNum().describe("Max files to process (default: 100)"),
|
|
351
|
-
}),
|
|
352
|
-
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
353
|
-
}, async (rawParams) => {
|
|
354
|
-
try {
|
|
355
|
-
const params = coerceParams(rawParams);
|
|
356
|
-
const replacements = JSON.parse(params.replacements);
|
|
357
|
-
if (!Array.isArray(replacements) || !replacements.length) throw new Error("replacements: non-empty JSON array of {old, new} required");
|
|
358
|
-
const result = bulkReplace(
|
|
359
|
-
params.path || process.cwd(),
|
|
360
|
-
params.glob || "**/*.{md,mjs,json,yml,ts,js}",
|
|
361
|
-
replacements,
|
|
362
|
-
{ dryRun: params.dry_run || false, maxFiles: params.max_files || 100 }
|
|
363
|
-
);
|
|
364
|
-
return { content: [{ type: "text", text: result }] };
|
|
365
|
-
} catch (e) {
|
|
366
|
-
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// --- Start ---
|
|
372
|
-
|
|
373
|
-
const transport = new StdioServerTransport();
|
|
374
|
-
await server.connect(transport);
|
|
375
|
-
void checkForUpdates("@levnikolaevich/hex-line-mcp", version);
|