@levnikolaevich/hex-line-mcp 1.17.0 → 1.18.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/dist/hook.mjs CHANGED
@@ -191,6 +191,11 @@ function isWithinDir(candidatePath, dirPath) {
191
191
 
192
192
  // hook.mjs
193
193
  import { fileURLToPath } from "node:url";
194
+ var HEX_LINE_MUTATING = /* @__PURE__ */ new Set([
195
+ "mcp__hex-line__edit_file",
196
+ "mcp__hex-line__write_file",
197
+ "mcp__hex-line__bulk_replace"
198
+ ]);
194
199
  function extOf(filePath) {
195
200
  const dot = filePath.lastIndexOf(".");
196
201
  return dot !== -1 ? filePath.slice(dot).toLowerCase() : "";
@@ -403,7 +408,9 @@ function block(reason, context) {
403
408
  ${context}` : reason;
404
409
  const output = {
405
410
  hookSpecificOutput: {
406
- permissionDecision: "deny"
411
+ hookEventName: "PreToolUse",
412
+ permissionDecision: "deny",
413
+ permissionDecisionReason: reason
407
414
  },
408
415
  systemMessage: msg
409
416
  };
@@ -413,7 +420,8 @@ ${context}` : reason;
413
420
  function advise(reason, context) {
414
421
  const output = {
415
422
  hookSpecificOutput: {
416
- permissionDecision: "allow"
423
+ hookEventName: "PreToolUse",
424
+ additionalContext: reason
417
425
  },
418
426
  systemMessage: context ? `${reason}
419
427
  ${context}` : reason
@@ -430,6 +438,12 @@ function redirect(reason, context) {
430
438
  function handlePreToolUse(data) {
431
439
  const toolName = data.tool_name || "";
432
440
  const toolInput = data.tool_input || {};
441
+ if (data.permission_mode === "plan" && HEX_LINE_MUTATING.has(toolName)) {
442
+ block(
443
+ "PLAN_MODE: hex-line write tools are blocked during planning.",
444
+ "Use read-only tools: read_file, grep_search, outline, verify, inspect_path, changes."
445
+ );
446
+ }
433
447
  if (toolName.startsWith("mcp__hex-line__")) {
434
448
  process.exit(0);
435
449
  }
@@ -576,6 +590,22 @@ function handleSessionStart() {
576
590
  const msg = styleActive ? "Hex-line MCP available. Output style active.\n<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <note>Follow the active hex-line output style for primary tool choices.</note>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, and text paths outside the current project root.</exceptions>\n</hex-line_instructions>" : "Hex-line MCP available.\n<hex-line_instructions>\n <deferred_loading>If hex-line schemas not loaded, run: ToolSearch('+hex-line read edit')</deferred_loading>\n <exploration>\n <rule>Use outline for structure (code + markdown), not Read. ~10-20 lines vs hundreds.</rule>\n <rule>Use read_file with offset/limit or ranges for targeted reads.</rule>\n <rule>Use grep_search before editing to get hash anchors.</rule>\n </exploration>\n <editing>\n <path name='surgical'>grep_search \u2192 edit_file (fastest: hash-verified, no full read needed)</path>\n <path name='exploratory'>outline \u2192 read_file (ranges) \u2192 edit_file with base_revision</path>\n <path name='multi-file'>bulk_replace(path=&quot;&lt;project root&gt;&quot;) for text rename/refactor across files</path>\n </editing>\n <tips>\n <tip>Auto-fill path from the active file or project root. Read-only tools may inspect explicit temp-file paths outside the repo. Mutating tools stay project-scoped unless you intentionally pass allow_external=true.</tip>\n <tip>Never invent range_checksum. Copy it from fresh read_file or grep_search blocks.</tip>\n <tip>Prefer set_line or insert_after for small local changes and replace_between for larger bounded rewrites.</tip>\n <tip>Carry revision from read_file into base_revision on edit_file.</tip>\n <tip>If edit returns CONFLICT, call verify \u2014 only reread when STALE.</tip>\n <tip>Avoid large first-pass edit batches. Start with 1-2 hunks, then continue from the returned revision.</tip>\n <tip>Use write_file for new files (no prior Read needed).</tip>\n <tip>Use inspect_path(pattern=...) for project file discovery and name/path globbing.</tip>\n </tips>\n <exceptions>Built-in tools stay OK for images, PDFs, notebooks, and text paths outside the current project root.</exceptions>\n</hex-line_instructions>";
577
591
  safeExit(1, JSON.stringify({ systemMessage: msg }), 0);
578
592
  }
593
+ function handleConfigChange(data) {
594
+ const source = data.source || "";
595
+ if (["project_settings", "local_settings", "user_settings", "policy_settings"].includes(source)) {
596
+ _hookMode = void 0;
597
+ _hexLineDisabled = null;
598
+ }
599
+ process.exit(0);
600
+ }
601
+ function handlePermissionDenied(data) {
602
+ const reason = data.reason || "";
603
+ const toolName = data.tool_name || "";
604
+ if (reason.includes("mcp__hex-line__") || toolName === "Bash") {
605
+ debugLog("PERMISSION_DENIED_AFTER_HEX_HINT", `${toolName}: ${reason.slice(0, 120)}`);
606
+ }
607
+ process.exit(0);
608
+ }
579
609
  var _norm = (p) => p.replace(/\\/g, "/");
580
610
  if (_norm(process.argv[1]) === _norm(fileURLToPath(import.meta.url))) {
581
611
  let input = "";
@@ -586,6 +616,9 @@ if (_norm(process.argv[1]) === _norm(fileURLToPath(import.meta.url))) {
586
616
  try {
587
617
  const data = JSON.parse(input);
588
618
  const event = data.hook_event_name || "";
619
+ if (event === "ConfigChange") {
620
+ handleConfigChange(data);
621
+ }
589
622
  if (isHexLineDisabled()) {
590
623
  if (event === "PreToolUse") handlePreToolUseReverse(data);
591
624
  process.exit(0);
@@ -593,6 +626,7 @@ if (_norm(process.argv[1]) === _norm(fileURLToPath(import.meta.url))) {
593
626
  if (event === "SessionStart") handleSessionStart();
594
627
  else if (event === "PreToolUse") handlePreToolUse(data);
595
628
  else if (event === "PostToolUse") handlePostToolUse(data);
629
+ else if (event === "PermissionDenied") handlePermissionDenied(data);
596
630
  else process.exit(0);
597
631
  } catch {
598
632
  process.exit(0);
package/dist/server.mjs CHANGED
@@ -3504,13 +3504,13 @@ async function fileOutline(filePath) {
3504
3504
  }
3505
3505
  const snapshot = readSnapshot(real);
3506
3506
  const isMarkdown = MARKDOWN_EXTS.has(ext);
3507
- const result = isMarkdown ? null : await outlineFromContent(snapshot.content, ext);
3507
+ const result2 = isMarkdown ? null : await outlineFromContent(snapshot.content, ext);
3508
3508
  let entries;
3509
3509
  let skippedRanges = [];
3510
3510
  let note = "";
3511
- if (result && result.entries.length > 0) {
3512
- entries = result.entries;
3513
- skippedRanges = result.skippedRanges;
3511
+ if (result2 && result2.entries.length > 0) {
3512
+ entries = result2.entries;
3513
+ skippedRanges = result2.skippedRanges;
3514
3514
  } else if (isMarkdown) {
3515
3515
  entries = markdownOutline(snapshot.lines);
3516
3516
  } else {
@@ -4722,8 +4722,34 @@ OUTPUT_CAPPED: Output exceeded ${MAX_BULK_OUTPUT_CHARS} chars.`;
4722
4722
  return output;
4723
4723
  }
4724
4724
 
4725
+ // ../hex-common/src/runtime/results.mjs
4726
+ var LARGE_RESULT_META = { "anthropic/maxResultSizeChars": 5e5 };
4727
+ function result(structured, { large = false } = {}) {
4728
+ const text = JSON.stringify(structured);
4729
+ const response = {
4730
+ content: [{ type: "text", text }],
4731
+ structuredContent: structured
4732
+ };
4733
+ if (large) response._meta = LARGE_RESULT_META;
4734
+ if (structured.status === "ERROR") response.isError = true;
4735
+ return response;
4736
+ }
4737
+ function errorResult(code, message, recovery, { large = false, extra = null } = {}) {
4738
+ const payload = {
4739
+ status: "ERROR",
4740
+ error: { code, message, recovery },
4741
+ summary: message
4742
+ };
4743
+ if (extra && typeof extra === "object") {
4744
+ Object.assign(payload, extra);
4745
+ }
4746
+ return result(payload, { large });
4747
+ }
4748
+
4725
4749
  // server.mjs
4726
- var version = true ? "1.17.0" : (await null).createRequire(import.meta.url)("./package.json").version;
4750
+ var version = true ? "1.18.0" : (await null).createRequire(import.meta.url)("./package.json").version;
4751
+ var STATUS_ENUM = z2.enum(["OK", "ERROR", "AUTO_REBASED", "CONFLICT", "STALE", "INVALID", "NO_CHANGES", "CHANGED", "UNSUPPORTED"]);
4752
+ var ERROR_SHAPE = z2.object({ code: z2.string(), message: z2.string(), recovery: z2.string() }).optional();
4727
4753
  var { server, StdioServerTransport } = await createServerRuntime({
4728
4754
  name: "hex-line-mcp",
4729
4755
  version
@@ -4759,7 +4785,16 @@ server.registerTool("read_file", {
4759
4785
  verbosity: z2.enum(["minimal", "compact", "full"]).optional().describe("Response budget. `minimal` is discovery-first, `compact` adds revision context, `full` preserves the richest payload."),
4760
4786
  edit_ready: flexBool().describe("Include hash/checksum edit protocol blocks explicitly. Default: false for discovery reads.")
4761
4787
  }),
4762
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
4788
+ outputSchema: z2.object({
4789
+ status: STATUS_ENUM,
4790
+ path: z2.string().optional(),
4791
+ paths: z2.array(z2.string()).optional(),
4792
+ content: z2.string().optional(),
4793
+ edit_ready: z2.boolean().optional(),
4794
+ next_action: z2.string().optional(),
4795
+ error: ERROR_SHAPE
4796
+ }),
4797
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
4763
4798
  }, async (rawParams) => {
4764
4799
  const { path: p, paths: multi, offset, limit, ranges: rawRanges, plain, verbosity, edit_ready } = rawParams ?? {};
4765
4800
  try {
@@ -4790,19 +4825,21 @@ next_hint: read_file path="${fp}" verbosity="compact"`);
4790
4825
  ERROR: ${e.message}`);
4791
4826
  }
4792
4827
  }
4793
- return { content: [{ type: "text", text: results.join("\n\n---\n\n") }] };
4828
+ const content2 = results.join("\n\n---\n\n");
4829
+ return result({ status: "OK", paths: multi, content: content2, edit_ready: !!edit_ready }, { large: !!edit_ready || readVerbosity === "full" || content2.length > 5e4 });
4794
4830
  }
4795
4831
  if (!p) throw new Error("Either 'path' or 'paths' is required");
4796
- return { content: [{ type: "text", text: readFile2(p, {
4832
+ const content = readFile2(p, {
4797
4833
  offset,
4798
4834
  limit: readLimit,
4799
4835
  ranges,
4800
4836
  plain: readPlain,
4801
4837
  verbosity: readVerbosity,
4802
4838
  editReady: !!edit_ready
4803
- }) }] };
4839
+ });
4840
+ return result({ status: "OK", path: p, content, edit_ready: !!edit_ready }, { large: !!edit_ready || readVerbosity === "full" || content.length > 5e4 });
4804
4841
  } catch (e) {
4805
- return { content: [{ type: "text", text: e.message }], isError: true };
4842
+ return errorResult(e.code || "READ_ERROR", e.message, e.recovery || "Check path and permissions");
4806
4843
  }
4807
4844
  });
4808
4845
  server.registerTool("edit_file", {
@@ -4819,7 +4856,8 @@ server.registerTool("edit_file", {
4819
4856
  conflict_policy: z2.enum(["strict", "conservative"]).optional().describe('Conflict handling (default: "conservative"). "conservative" returns structured CONFLICT output with recovery_ranges, retry_edit/retry_edits, suggested_read_call, and retry_plan when available.'),
4820
4857
  allow_external: flexBool().describe("Allow editing a path outside the current project root. Use only when you intentionally target a temp or external file.")
4821
4858
  }),
4822
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
4859
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
4860
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
4823
4861
  }, async (rawParams) => {
4824
4862
  const { path: p, edits: json, dry_run, restore_indent, base_revision, conflict_policy, allow_external } = rawParams ?? {};
4825
4863
  try {
@@ -4831,19 +4869,15 @@ server.registerTool("edit_file", {
4831
4869
  throw new Error('edits: invalid JSON. Expected: [{"set_line":{"anchor":"xx.N","new_text":"..."}}]');
4832
4870
  }
4833
4871
  if (!Array.isArray(parsed) || !parsed.length) throw new Error("Edits: non-empty JSON array required");
4834
- return {
4835
- content: [{
4836
- type: "text",
4837
- text: editFile(p, parsed, {
4838
- dryRun: dry_run,
4839
- restoreIndent: restore_indent,
4840
- baseRevision: base_revision,
4841
- conflictPolicy: conflict_policy
4842
- })
4843
- }]
4844
- };
4872
+ const content = editFile(p, parsed, {
4873
+ dryRun: dry_run,
4874
+ restoreIndent: restore_indent,
4875
+ baseRevision: base_revision,
4876
+ conflictPolicy: conflict_policy
4877
+ });
4878
+ return result({ status: "OK", path: p, content }, { large: content.length > 5e4 });
4845
4879
  } catch (e) {
4846
- return { content: [{ type: "text", text: e.message }], isError: true };
4880
+ return errorResult(e.code || "EDIT_ERROR", e.message, e.recovery || "Check anchors and checksums");
4847
4881
  }
4848
4882
  });
4849
4883
  server.registerTool("write_file", {
@@ -4854,7 +4888,8 @@ server.registerTool("write_file", {
4854
4888
  content: z2.string().describe("File content"),
4855
4889
  allow_external: flexBool().describe("Allow writing a path outside the current project root. Use only when you intentionally target a temp or external file.")
4856
4890
  }),
4857
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
4891
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), lines: z2.number().optional(), error: ERROR_SHAPE }),
4892
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false }
4858
4893
  }, async (rawParams) => {
4859
4894
  const { path: p, content, allow_external } = rawParams ?? {};
4860
4895
  try {
@@ -4862,9 +4897,10 @@ server.registerTool("write_file", {
4862
4897
  const abs = validateWritePath(p);
4863
4898
  mkdirSync2(dirname6(abs), { recursive: true });
4864
4899
  writeFileSync4(abs, content, "utf-8");
4865
- return { content: [{ type: "text", text: `Created ${p} (${content.split("\n").length} lines)` }] };
4900
+ const lines = content.split("\n").length;
4901
+ return result({ status: "OK", path: p, lines });
4866
4902
  } catch (e) {
4867
- return { content: [{ type: "text", text: e.message }], isError: true };
4903
+ return errorResult(e.code || "WRITE_ERROR", e.message, e.recovery || "Check path and permissions");
4868
4904
  }
4869
4905
  });
4870
4906
  server.registerTool("grep_search", {
@@ -4889,7 +4925,8 @@ server.registerTool("grep_search", {
4889
4925
  edit_ready: flexBool().describe("Preserve hash/checksum search hunks in `content` mode. Default: false."),
4890
4926
  allow_large_output: flexBool().describe("Bypass the default content-mode block/char caps when you intentionally need a larger payload.")
4891
4927
  }),
4892
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
4928
+ outputSchema: z2.object({ status: STATUS_ENUM, pattern: z2.string().optional(), content: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
4929
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
4893
4930
  }, async (rawParams) => {
4894
4931
  const {
4895
4932
  pattern,
@@ -4914,7 +4951,7 @@ server.registerTool("grep_search", {
4914
4951
  const resolvedOutput = output ?? "summary";
4915
4952
  const resolvedLimit = limit ?? (resolvedOutput === "summary" ? 20 : 100);
4916
4953
  const resolvedTotalLimit = total_limit ?? (resolvedOutput === "summary" ? 50 : void 0);
4917
- const result = await grepSearch(pattern, {
4954
+ const searchResult = await grepSearch(pattern, {
4918
4955
  path: p,
4919
4956
  glob,
4920
4957
  type,
@@ -4932,9 +4969,9 @@ server.registerTool("grep_search", {
4932
4969
  editReady: !!edit_ready,
4933
4970
  allowLargeOutput: !!allow_large_output
4934
4971
  });
4935
- return { content: [{ type: "text", text: result }] };
4972
+ return result({ status: "OK", pattern, content: searchResult }, { large: !!allow_large_output || resolvedOutput === "content" });
4936
4973
  } catch (e) {
4937
- return { content: [{ type: "text", text: e.message }], isError: true };
4974
+ return errorResult(e.code || "GREP_ERROR", e.message, e.recovery || "Check pattern syntax");
4938
4975
  }
4939
4976
  });
4940
4977
  server.registerTool("outline", {
@@ -4943,14 +4980,15 @@ server.registerTool("outline", {
4943
4980
  inputSchema: z2.object({
4944
4981
  path: z2.string().describe("Source file path")
4945
4982
  }),
4946
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
4983
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), error: ERROR_SHAPE }),
4984
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
4947
4985
  }, async (rawParams) => {
4948
4986
  const { path: p } = rawParams ?? {};
4949
4987
  try {
4950
- const result = await fileOutline(p);
4951
- return { content: [{ type: "text", text: result }] };
4988
+ const content = await fileOutline(p);
4989
+ return result({ status: "OK", path: p, content });
4952
4990
  } catch (e) {
4953
- return { content: [{ type: "text", text: e.message }], isError: true };
4991
+ return errorResult(e.code || "OUTLINE_ERROR", e.message, e.recovery || "Check file path and language support");
4954
4992
  }
4955
4993
  });
4956
4994
  server.registerTool("verify", {
@@ -4961,16 +4999,18 @@ server.registerTool("verify", {
4961
4999
  checksums: z2.array(z2.string()).describe('Checksum strings, e.g. ["1-50:f7e2a1b0", "51-100:abcd1234"]'),
4962
5000
  base_revision: z2.string().optional().describe("Optional prior revision to compare against latest state.")
4963
5001
  }),
4964
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
5002
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), error: ERROR_SHAPE }),
5003
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
4965
5004
  }, async (rawParams) => {
4966
5005
  const { path: p, checksums, base_revision } = rawParams ?? {};
4967
5006
  try {
4968
5007
  if (!Array.isArray(checksums) || checksums.length === 0) {
4969
5008
  throw new Error("checksums must be a non-empty array of strings");
4970
5009
  }
4971
- return { content: [{ type: "text", text: verifyChecksums(p, checksums, { baseRevision: base_revision }) }] };
5010
+ const content = verifyChecksums(p, checksums, { baseRevision: base_revision });
5011
+ return result({ status: "OK", path: p, content });
4972
5012
  } catch (e) {
4973
- return { content: [{ type: "text", text: e.message }], isError: true };
5013
+ return errorResult(e.code || "VERIFY_ERROR", e.message, e.recovery || "Check checksums format");
4974
5014
  }
4975
5015
  });
4976
5016
  server.registerTool("inspect_path", {
@@ -4986,21 +5026,23 @@ server.registerTool("inspect_path", {
4986
5026
  format: z2.enum(["compact", "full"]).optional().describe('"compact" = shorter path view, "full" = include sizes/metadata where available'),
4987
5027
  verbosity: z2.enum(["minimal", "compact", "full"]).optional().describe("Response budget. `minimal` returns the shortest tree summary.")
4988
5028
  }),
4989
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
5029
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5030
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
4990
5031
  }, async (rawParams) => {
4991
5032
  const { path: p, max_depth, max_entries, gitignore, format, pattern, type: entryType, verbosity } = rawParams ?? {};
4992
5033
  try {
4993
5034
  const resolvedVerbosity = verbosity ?? "minimal";
4994
- return { content: [{ type: "text", text: inspectPath(p, {
5035
+ const content = inspectPath(p, {
4995
5036
  max_depth: max_depth ?? (pattern ? 20 : resolvedVerbosity === "full" ? 3 : 2),
4996
5037
  gitignore,
4997
5038
  format: format ?? (resolvedVerbosity === "full" ? "full" : "compact"),
4998
5039
  pattern,
4999
5040
  type: entryType,
5000
5041
  max_entries
5001
- }) }] };
5042
+ });
5043
+ return result({ status: "OK", path: p, content });
5002
5044
  } catch (e) {
5003
- return { content: [{ type: "text", text: e.message }], isError: true };
5045
+ return errorResult(e.code || "INSPECT_ERROR", e.message, e.recovery || "Check path exists");
5004
5046
  }
5005
5047
  });
5006
5048
  server.registerTool("changes", {
@@ -5010,13 +5052,15 @@ server.registerTool("changes", {
5010
5052
  path: z2.string().describe("File or directory path"),
5011
5053
  compare_against: z2.string().optional().describe('Git ref to compare against (default: "HEAD")')
5012
5054
  }),
5013
- annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true }
5055
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5056
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
5014
5057
  }, async (rawParams) => {
5015
5058
  const { path: p, compare_against } = rawParams ?? {};
5016
5059
  try {
5017
- return { content: [{ type: "text", text: await fileChanges(p, compare_against) }] };
5060
+ const content = await fileChanges(p, compare_against);
5061
+ return result({ status: "OK", path: p, content }, { large: content.length > 5e4 });
5018
5062
  } catch (e) {
5019
- return { content: [{ type: "text", text: e.message }], isError: true };
5063
+ return errorResult(e.code || "CHANGES_ERROR", e.message, e.recovery || "Check git ref and path");
5020
5064
  }
5021
5065
  });
5022
5066
  server.registerTool("bulk_replace", {
@@ -5031,7 +5075,8 @@ server.registerTool("bulk_replace", {
5031
5075
  format: z2.enum(["compact", "full"]).optional().describe('"compact" (default) = summary only, "full" = include capped diffs'),
5032
5076
  allow_external: flexBool().describe("Allow a replacement root outside the current project root. Use only when you intentionally target a temp or external directory.")
5033
5077
  }),
5034
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false }
5078
+ outputSchema: z2.object({ status: STATUS_ENUM, path: z2.string().optional(), content: z2.string().optional(), next_action: z2.string().optional(), error: ERROR_SHAPE }),
5079
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false }
5035
5080
  }, async (rawParams) => {
5036
5081
  try {
5037
5082
  const params = rawParams ?? {};
@@ -5044,15 +5089,15 @@ server.registerTool("bulk_replace", {
5044
5089
  throw new Error('replacements: invalid JSON. Expected: [{"old":"text","new":"replacement"}]');
5045
5090
  }
5046
5091
  const replacements = replacementPairsSchema.parse(replacementsInput);
5047
- const result = bulkReplace(
5092
+ const content = bulkReplace(
5048
5093
  params.path,
5049
5094
  params.glob || "**/*.{md,mjs,json,yml,ts,js}",
5050
5095
  replacements,
5051
5096
  { dryRun: params.dry_run || false, maxFiles: params.max_files || 100, format: params.format }
5052
5097
  );
5053
- return { content: [{ type: "text", text: result }] };
5098
+ return result({ status: "OK", path: params.path, content }, { large: params.format === "full" });
5054
5099
  } catch (e) {
5055
- return { content: [{ type: "text", text: e.message }], isError: true };
5100
+ return errorResult(e.code || "REPLACE_ERROR", e.message, e.recovery || "Check replacement pairs and scope");
5056
5101
  }
5057
5102
  });
5058
5103
  var transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levnikolaevich/hex-line-mcp",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "mcpName": "io.github.levnikolaevich/hex-line-mcp",
5
5
  "type": "module",
6
6
  "description": "Hash-verified file editing MCP + token efficiency hook for AI coding agents. 9 tools: inspect_path, read, edit, write, grep, outline, verify, changes, bulk_replace.",