@levnikolaevich/hex-line-mcp 1.3.1 → 1.3.3
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/README.md +120 -47
- package/benchmark/atomic.mjs +502 -0
- package/benchmark/graph.mjs +80 -0
- package/benchmark/index.mjs +144 -0
- package/benchmark/workflows.mjs +350 -0
- package/hook.mjs +48 -15
- package/lib/benchmark-helpers.mjs +1 -1
- package/lib/changes.mjs +2 -1
- package/lib/coerce.mjs +1 -42
- package/lib/edit.mjs +258 -248
- package/lib/graph-enrich.mjs +76 -58
- package/lib/hash.mjs +1 -109
- package/lib/info.mjs +1 -1
- package/lib/normalize.mjs +1 -106
- package/lib/outline.mjs +32 -87
- package/lib/read.mjs +8 -5
- package/lib/revisions.mjs +238 -0
- package/lib/search.mjs +6 -7
- package/lib/security.mjs +4 -4
- package/lib/setup.mjs +7 -20
- package/lib/update-check.mjs +1 -56
- package/lib/verify.mjs +32 -16
- package/output-style.md +21 -6
- package/package.json +18 -6
- package/server.mjs +35 -43
- package/benchmark.mjs +0 -1106
package/server.mjs
CHANGED
|
@@ -13,20 +13,16 @@ import { dirname } from "node:path";
|
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
const { version } = createRequire(import.meta.url)("./package.json");
|
|
15
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";
|
|
16
20
|
// LLM clients may send booleans as strings ("true"/"false").
|
|
17
21
|
// z.coerce.boolean() is unsafe: Boolean("false") === true.
|
|
18
|
-
const flexBool = () => z.preprocess(
|
|
19
|
-
v => typeof v === "string" ? v === "true" : v,
|
|
20
|
-
z.boolean().optional()
|
|
21
|
-
).optional();
|
|
22
22
|
// LLM clients may send numbers as strings ("5" instead of 5).
|
|
23
23
|
// z.coerce.number() generates {"type":"number"} → strict MCP clients reject strings.
|
|
24
24
|
// flexNum generates schema accepting both, coerces at runtime.
|
|
25
25
|
// Outer .optional() ensures JSON Schema marks field as not-required.
|
|
26
|
-
const flexNum = () => z.preprocess(
|
|
27
|
-
v => typeof v === "string" ? Number(v) : v,
|
|
28
|
-
z.number().optional()
|
|
29
|
-
).optional();
|
|
30
26
|
|
|
31
27
|
import { readFile } from "./lib/read.mjs";
|
|
32
28
|
import { editFile } from "./lib/edit.mjs";
|
|
@@ -39,24 +35,12 @@ import { fileInfo } from "./lib/info.mjs";
|
|
|
39
35
|
import { setupHooks } from "./lib/setup.mjs";
|
|
40
36
|
import { fileChanges } from "./lib/changes.mjs";
|
|
41
37
|
import { bulkReplace } from "./lib/bulk-replace.mjs";
|
|
42
|
-
import { coerceParams } from "./lib/coerce.mjs";
|
|
43
|
-
import { checkForUpdates } from "./lib/update-check.mjs";
|
|
44
38
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
({ StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js"));
|
|
51
|
-
} catch {
|
|
52
|
-
process.stderr.write(
|
|
53
|
-
"hex-line-mcp: @modelcontextprotocol/sdk not found.\n" +
|
|
54
|
-
"Run: cd mcp/hex-line-mcp && npm install\n"
|
|
55
|
-
);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const server = new McpServer({ name: "hex-line-mcp", version });
|
|
39
|
+
const { server, StdioServerTransport } = await createServerRuntime({
|
|
40
|
+
name: "hex-line-mcp",
|
|
41
|
+
version,
|
|
42
|
+
installDir: "mcp/hex-line-mcp",
|
|
43
|
+
});
|
|
60
44
|
|
|
61
45
|
|
|
62
46
|
// ==================== read_file ====================
|
|
@@ -64,10 +48,8 @@ const server = new McpServer({ name: "hex-line-mcp", version });
|
|
|
64
48
|
server.registerTool("read_file", {
|
|
65
49
|
title: "Read File",
|
|
66
50
|
description:
|
|
67
|
-
"Read a file with
|
|
68
|
-
"
|
|
69
|
-
"For files >100 lines: ALWAYS use outline first, then read_file with offset/limit for specific sections. " +
|
|
70
|
-
"Reading a 500+ line file in full wastes 75% of context tokens.",
|
|
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.",
|
|
71
53
|
inputSchema: z.object({
|
|
72
54
|
path: z.string().optional().describe("File or directory path"),
|
|
73
55
|
paths: z.array(z.string()).optional().describe("Array of file paths to read (batch mode)"),
|
|
@@ -103,30 +85,40 @@ server.registerTool("read_file", {
|
|
|
103
85
|
server.registerTool("edit_file", {
|
|
104
86
|
title: "Edit File",
|
|
105
87
|
description:
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"replace: unique text match, or all:true for rename. insert_after: add below anchor.",
|
|
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.",
|
|
110
91
|
inputSchema: z.object({
|
|
111
92
|
path: z.string().describe("File to edit"),
|
|
112
93
|
edits: z.string().describe(
|
|
113
94
|
'JSON array. Examples:\n' +
|
|
114
95
|
'{"set_line":{"anchor":"ab.12","new_text":"new"}} — replace line\n' +
|
|
115
96
|
'{"replace_lines":{"start_anchor":"ab.10","end_anchor":"cd.15","new_text":"...","range_checksum":"10-15:a1b2c3d4"}} — range\n' +
|
|
116
|
-
'{"
|
|
117
|
-
'{"
|
|
118
|
-
'{"replace":{"old_text":"find","new_text":"replace","all":true}} — replace all',
|
|
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.',
|
|
119
99
|
),
|
|
120
100
|
dry_run: flexBool().describe("Preview changes without writing"),
|
|
121
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.'),
|
|
122
104
|
}),
|
|
123
105
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
124
106
|
}, async (rawParams) => {
|
|
125
|
-
const { path: p, edits: json, dry_run, restore_indent } = coerceParams(rawParams);
|
|
107
|
+
const { path: p, edits: json, dry_run, restore_indent, base_revision, conflict_policy } = coerceParams(rawParams);
|
|
126
108
|
try {
|
|
127
109
|
const parsed = JSON.parse(json);
|
|
128
110
|
if (!Array.isArray(parsed) || !parsed.length) throw new Error("Edits: non-empty JSON array required");
|
|
129
|
-
return {
|
|
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
|
+
};
|
|
130
122
|
} catch (e) {
|
|
131
123
|
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
132
124
|
}
|
|
@@ -229,19 +221,19 @@ server.registerTool("outline", {
|
|
|
229
221
|
server.registerTool("verify", {
|
|
230
222
|
title: "Verify Checksums",
|
|
231
223
|
description:
|
|
232
|
-
"Check
|
|
233
|
-
"Single-line response when nothing changed. Use to check file staleness without re-reading.",
|
|
224
|
+
"Check whether held checksums and optional base_revision are still current, without rereading the file.",
|
|
234
225
|
inputSchema: z.object({
|
|
235
226
|
path: z.string().describe("File path"),
|
|
236
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."),
|
|
237
229
|
}),
|
|
238
230
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
239
231
|
}, async (rawParams) => {
|
|
240
|
-
const { path: p, checksums } = coerceParams(rawParams);
|
|
232
|
+
const { path: p, checksums, base_revision } = coerceParams(rawParams);
|
|
241
233
|
try {
|
|
242
234
|
const parsed = JSON.parse(checksums);
|
|
243
235
|
if (!Array.isArray(parsed)) throw new Error("checksums must be a JSON array of strings");
|
|
244
|
-
return { content: [{ type: "text", text: verifyChecksums(p, parsed) }] };
|
|
236
|
+
return { content: [{ type: "text", text: verifyChecksums(p, parsed, { baseRevision: base_revision }) }] };
|
|
245
237
|
} catch (e) {
|
|
246
238
|
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
247
239
|
}
|
|
@@ -349,7 +341,7 @@ server.registerTool("bulk_replace", {
|
|
|
349
341
|
title: "Bulk Replace",
|
|
350
342
|
description:
|
|
351
343
|
"Search-and-replace across multiple files. Finds files by glob, applies ordered text replacements, returns per-file diffs. " +
|
|
352
|
-
"Use dry_run:true to preview. For single-file
|
|
344
|
+
"Use dry_run:true to preview. For single-file rename, set glob to the filename.",
|
|
353
345
|
inputSchema: z.object({
|
|
354
346
|
replacements: z.string().describe('JSON array of {old, new} pairs: [{"old":"foo","new":"bar"}]'),
|
|
355
347
|
glob: z.string().optional().describe('File glob (default: "**/*.{md,mjs,json,yml,ts,js}")'),
|