md-feedback 1.1.0 → 1.2.1

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.
Files changed (3) hide show
  1. package/README.md +5 -0
  2. package/dist/mcp-server.js +224 -140
  3. package/package.json +73 -70
package/README.md CHANGED
@@ -52,6 +52,11 @@ That's it. No install, no setup — `npx` handles everything.
52
52
  | `batch_apply` | Apply multiple operations in a single transaction |
53
53
  | `get_memo_changes` | Get implementation history and progress for a memo |
54
54
 
55
+ ## Safety & Reliability
56
+
57
+ - **File mutex** — concurrent MCP tool calls are serialized per-file, preventing data corruption
58
+ - **Improved anchor matching** — annotations find their intended location more reliably, even with multiple matches
59
+
55
60
  ## How It Works
56
61
 
57
62
  1. You annotate a markdown plan in the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=yeominux.md-feedback-vscode)
@@ -3224,8 +3224,8 @@ var require_utils = __commonJS({
3224
3224
  }
3225
3225
  return ind;
3226
3226
  }
3227
- function removeDotSegments(path2) {
3228
- let input = path2;
3227
+ function removeDotSegments(path3) {
3228
+ let input = path3;
3229
3229
  const output = [];
3230
3230
  let nextSlash = -1;
3231
3231
  let len = 0;
@@ -3424,8 +3424,8 @@ var require_schemes = __commonJS({
3424
3424
  wsComponent.secure = void 0;
3425
3425
  }
3426
3426
  if (wsComponent.resourceName) {
3427
- const [path2, query] = wsComponent.resourceName.split("?");
3428
- wsComponent.path = path2 && path2 !== "/" ? path2 : void 0;
3427
+ const [path3, query] = wsComponent.resourceName.split("?");
3428
+ wsComponent.path = path3 && path3 !== "/" ? path3 : void 0;
3429
3429
  wsComponent.query = query;
3430
3430
  wsComponent.resourceName = void 0;
3431
3431
  }
@@ -7276,8 +7276,8 @@ function getErrorMap() {
7276
7276
 
7277
7277
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
7278
7278
  var makeIssue = (params) => {
7279
- const { data, path: path2, errorMaps, issueData } = params;
7280
- const fullPath = [...path2, ...issueData.path || []];
7279
+ const { data, path: path3, errorMaps, issueData } = params;
7280
+ const fullPath = [...path3, ...issueData.path || []];
7281
7281
  const fullIssue = {
7282
7282
  ...issueData,
7283
7283
  path: fullPath
@@ -7393,11 +7393,11 @@ var errorUtil;
7393
7393
 
7394
7394
  // ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js
7395
7395
  var ParseInputLazyPath = class {
7396
- constructor(parent, value, path2, key) {
7396
+ constructor(parent, value, path3, key) {
7397
7397
  this._cachedPath = [];
7398
7398
  this.parent = parent;
7399
7399
  this.data = value;
7400
- this._path = path2;
7400
+ this._path = path3;
7401
7401
  this._key = key;
7402
7402
  }
7403
7403
  get path() {
@@ -11035,10 +11035,10 @@ function assignProp(target, prop, value) {
11035
11035
  configurable: true
11036
11036
  });
11037
11037
  }
11038
- function getElementAtPath(obj, path2) {
11039
- if (!path2)
11038
+ function getElementAtPath(obj, path3) {
11039
+ if (!path3)
11040
11040
  return obj;
11041
- return path2.reduce((acc, key) => acc?.[key], obj);
11041
+ return path3.reduce((acc, key) => acc?.[key], obj);
11042
11042
  }
11043
11043
  function promiseAllObject(promisesObj) {
11044
11044
  const keys = Object.keys(promisesObj);
@@ -11358,11 +11358,11 @@ function aborted(x, startIndex = 0) {
11358
11358
  }
11359
11359
  return false;
11360
11360
  }
11361
- function prefixIssues(path2, issues) {
11361
+ function prefixIssues(path3, issues) {
11362
11362
  return issues.map((iss) => {
11363
11363
  var _a;
11364
11364
  (_a = iss).path ?? (_a.path = []);
11365
- iss.path.unshift(path2);
11365
+ iss.path.unshift(path3);
11366
11366
  return iss;
11367
11367
  });
11368
11368
  }
@@ -20848,6 +20848,7 @@ var StdioServerTransport = class {
20848
20848
  // src/file-ops.ts
20849
20849
  var import_fs = require("fs");
20850
20850
  var import_path = require("path");
20851
+ var import_node_crypto = require("node:crypto");
20851
20852
  function resolvePath(filePath) {
20852
20853
  return (0, import_path.isAbsolute)(filePath) ? filePath : (0, import_path.resolve)(process.cwd(), filePath);
20853
20854
  }
@@ -20864,9 +20865,16 @@ function readMarkdownFile(filePath) {
20864
20865
  }
20865
20866
  function writeMarkdownFile(filePath, content) {
20866
20867
  const resolved = resolvePath(filePath);
20868
+ const dir = (0, import_path.dirname)(resolved);
20869
+ const tmpPath = (0, import_path.join)(dir, `.mf-tmp-${(0, import_node_crypto.randomBytes)(6).toString("hex")}`);
20867
20870
  try {
20868
- (0, import_fs.writeFileSync)(resolved, content, "utf-8");
20871
+ (0, import_fs.writeFileSync)(tmpPath, content, "utf-8");
20872
+ (0, import_fs.renameSync)(tmpPath, resolved);
20869
20873
  } catch (err) {
20874
+ try {
20875
+ (0, import_fs.unlinkSync)(tmpPath);
20876
+ } catch {
20877
+ }
20870
20878
  throw new Error(`Cannot write file ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
20871
20879
  }
20872
20880
  }
@@ -20887,6 +20895,13 @@ function writeSnapshot(mdFilePath, content) {
20887
20895
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
20888
20896
  const snapshotPath = (0, import_path.join)(snapshotsDir, `snapshot-${ts}.md`);
20889
20897
  (0, import_fs.writeFileSync)(snapshotPath, content, "utf-8");
20898
+ const files = (0, import_fs.readdirSync)(snapshotsDir).filter((f) => f.startsWith("snapshot-")).sort();
20899
+ while (files.length > 20) {
20900
+ try {
20901
+ (0, import_fs.unlinkSync)((0, import_path.join)(snapshotsDir, files.shift()));
20902
+ } catch {
20903
+ }
20904
+ }
20890
20905
  return snapshotPath;
20891
20906
  }
20892
20907
  function readProgress(mdFilePath) {
@@ -21155,6 +21170,12 @@ function generateContext(title, filePath, sections, highlights, docMemos, target
21155
21170
  }
21156
21171
 
21157
21172
  // ../../packages/shared/src/document-writer.ts
21173
+ function escAttrValue(s) {
21174
+ return s.replace(/&/g, "&").replace(/"/g, """).replace(/\n/g, "
").replace(/-->/g, "-->");
21175
+ }
21176
+ function unescAttrValue(s) {
21177
+ return s.replace(/-->/g, "-->").replace(/
/g, "\n").replace(/"/g, '"').replace(/&/g, "&");
21178
+ }
21158
21179
  function hashLine(line) {
21159
21180
  let hash = 5381;
21160
21181
  for (let i = 0; i < line.length; i++) {
@@ -21184,11 +21205,10 @@ var ARTIFACT_START_RE = /^<!-- MEMO_ARTIFACT\s*$/;
21184
21205
  var ARTIFACT_END_RE = /^-->$/;
21185
21206
  var DEPENDENCY_RE = /^<!-- MEMO_DEPENDENCY\s+id="([^"]+)"\s+from="([^"]+)"\s+to="([^"]+)"\s+type="([^"]+)" -->$/;
21186
21207
  function parseAttrs(lines) {
21187
- const unesc = (s) => s.replace(/&#10;/g, "\n").replace(/&quot;/g, '"');
21188
21208
  const attrs = {};
21189
21209
  for (const line of lines) {
21190
21210
  const m = line.trim().match(/^(\w+)="([^"]*)"$/);
21191
- if (m) attrs[m[1]] = unesc(m[2]);
21211
+ if (m) attrs[m[1]] = unescAttrValue(m[2]);
21192
21212
  }
21193
21213
  return attrs;
21194
21214
  }
@@ -21209,7 +21229,6 @@ function splitDocument(markdown) {
21209
21229
  const dependencies = [];
21210
21230
  const checkpoints = [];
21211
21231
  const gates = [];
21212
- const unknownComments = [];
21213
21232
  let cursor = null;
21214
21233
  let openResponse = null;
21215
21234
  let i = 0;
@@ -21292,13 +21311,15 @@ function splitDocument(markdown) {
21292
21311
  }
21293
21312
  i++;
21294
21313
  const a = parseAttrs(attrLines);
21314
+ const override = a.override;
21295
21315
  gates.push({
21296
21316
  id: a.id || `gate_${Date.now()}`,
21297
21317
  type: a.type || "custom",
21298
21318
  status: a.status || "blocked",
21299
21319
  blockedBy: a.blockedBy ? a.blockedBy.split(",").map((s) => s.trim()).filter(Boolean) : [],
21300
21320
  canProceedIf: a.canProceedIf || "",
21301
- doneDefinition: a.doneDefinition || ""
21321
+ doneDefinition: a.doneDefinition || "",
21322
+ ...override ? { override } : {}
21302
21323
  });
21303
21324
  continue;
21304
21325
  }
@@ -21448,8 +21469,7 @@ function splitDocument(markdown) {
21448
21469
  dependencies,
21449
21470
  checkpoints,
21450
21471
  gates,
21451
- cursor,
21452
- unknownComments
21472
+ cursor
21453
21473
  };
21454
21474
  }
21455
21475
  function mergeDocument(parts) {
@@ -21480,71 +21500,70 @@ function mergeDocument(parts) {
21480
21500
  return sections.join("\n\n") + "\n";
21481
21501
  }
21482
21502
  function serializeMemoV2(memo) {
21483
- const esc2 = (s) => s.replace(/"/g, "&quot;").replace(/\n/g, "&#10;");
21484
21503
  return [
21485
21504
  "<!-- USER_MEMO",
21486
- ` id="${esc2(memo.id)}"`,
21505
+ ` id="${escAttrValue(memo.id)}"`,
21487
21506
  ` type="${memo.type}"`,
21488
21507
  ` status="${memo.status}"`,
21489
21508
  ` owner="${memo.owner}"`,
21490
- ` source="${esc2(memo.source)}"`,
21509
+ ` source="${escAttrValue(memo.source)}"`,
21491
21510
  ` color="${memo.color}"`,
21492
- ` text="${esc2(memo.text)}"`,
21493
- ` anchorText="${esc2(memo.anchorText)}"`,
21494
- ` anchor="${esc2(memo.anchor)}"`,
21511
+ ` text="${escAttrValue(memo.text)}"`,
21512
+ ` anchorText="${escAttrValue(memo.anchorText)}"`,
21513
+ ` anchor="${escAttrValue(memo.anchor)}"`,
21495
21514
  ` createdAt="${memo.createdAt}"`,
21496
21515
  ` updatedAt="${memo.updatedAt}"`,
21497
21516
  "-->"
21498
21517
  ].join("\n");
21499
21518
  }
21500
21519
  function serializeGate(gate) {
21501
- return [
21520
+ const lines = [
21502
21521
  "<!-- GATE",
21503
21522
  ` id="${gate.id}"`,
21504
21523
  ` type="${gate.type}"`,
21505
21524
  ` status="${gate.status}"`,
21506
21525
  ` blockedBy="${gate.blockedBy.join(",")}"`,
21507
- ` canProceedIf="${gate.canProceedIf.replace(/"/g, "&quot;")}"`,
21508
- ` doneDefinition="${gate.doneDefinition.replace(/"/g, "&quot;")}"`,
21509
- "-->"
21510
- ].join("\n");
21526
+ ` canProceedIf="${escAttrValue(gate.canProceedIf)}"`,
21527
+ ` doneDefinition="${escAttrValue(gate.doneDefinition)}"`
21528
+ ];
21529
+ if (gate.override) {
21530
+ lines.push(` override="${gate.override}"`);
21531
+ }
21532
+ lines.push("-->");
21533
+ return lines.join("\n");
21511
21534
  }
21512
21535
  function serializeCursor(cursor) {
21513
21536
  return [
21514
21537
  "<!-- PLAN_CURSOR",
21515
21538
  ` taskId="${cursor.taskId}"`,
21516
21539
  ` step="${cursor.step}"`,
21517
- ` nextAction="${cursor.nextAction.replace(/"/g, "&quot;")}"`,
21540
+ ` nextAction="${escAttrValue(cursor.nextAction)}"`,
21518
21541
  ` lastSeenHash="${cursor.lastSeenHash}"`,
21519
21542
  ` updatedAt="${cursor.updatedAt}"`,
21520
21543
  "-->"
21521
21544
  ].join("\n");
21522
21545
  }
21523
21546
  function serializeCheckpoint(cp) {
21524
- const note = cp.note.replace(/"/g, "&quot;");
21525
21547
  const sections = cp.sectionsReviewed.join(",");
21526
- return `<!-- CHECKPOINT id="${cp.id}" time="${cp.timestamp}" note="${note}" fixes=${cp.fixes} questions=${cp.questions} highlights=${cp.highlights} sections="${sections}" -->`;
21548
+ return `<!-- CHECKPOINT id="${cp.id}" time="${cp.timestamp}" note="${escAttrValue(cp.note)}" fixes=${cp.fixes} questions=${cp.questions} highlights=${cp.highlights} sections="${sections}" -->`;
21527
21549
  }
21528
21550
  function serializeMemoImpl(impl) {
21529
- const esc2 = (s) => s.replace(/"/g, "&quot;").replace(/\n/g, "&#10;");
21530
- const ops = JSON.stringify(impl.operations).replace(/"/g, "&quot;");
21531
21551
  return [
21532
21552
  "<!-- MEMO_IMPL",
21533
- ` id="${esc2(impl.id)}"`,
21534
- ` memoId="${esc2(impl.memoId)}"`,
21553
+ ` id="${escAttrValue(impl.id)}"`,
21554
+ ` memoId="${escAttrValue(impl.memoId)}"`,
21535
21555
  ` status="${impl.status}"`,
21536
- ` operations="${ops}"`,
21537
- ` summary="${esc2(impl.summary)}"`,
21556
+ ` operations="${escAttrValue(JSON.stringify(impl.operations))}"`,
21557
+ ` summary="${escAttrValue(impl.summary)}"`,
21538
21558
  ` appliedAt="${impl.appliedAt}"`,
21539
21559
  "-->"
21540
21560
  ].join("\n");
21541
21561
  }
21542
21562
  function serializeMemoArtifact(art) {
21543
- const esc2 = (s) => s.replace(/"/g, "&quot;");
21544
21563
  return [
21545
21564
  "<!-- MEMO_ARTIFACT",
21546
- ` id="${esc2(art.id)}"`,
21547
- ` memoId="${esc2(art.memoId)}"`,
21565
+ ` id="${escAttrValue(art.id)}"`,
21566
+ ` memoId="${escAttrValue(art.memoId)}"`,
21548
21567
  ` files="${art.files.join(",")}"`,
21549
21568
  ` linkedAt="${art.linkedAt}"`,
21550
21569
  "-->"
@@ -22136,12 +22155,16 @@ function evaluateGate(gate, memos) {
22136
22155
  function evaluateAllGates(gates, memos) {
22137
22156
  return gates.map((gate) => ({
22138
22157
  ...gate,
22139
- status: evaluateGate(gate, memos)
22158
+ status: gate.override || evaluateGate(gate, memos)
22140
22159
  }));
22141
22160
  }
22142
22161
 
22162
+ // src/tools.ts
22163
+ var import_node_fs2 = require("node:fs");
22164
+
22143
22165
  // src/file-safety.ts
22144
22166
  var import_node_path = __toESM(require("node:path"));
22167
+ var import_node_fs = require("node:fs");
22145
22168
  var DEFAULT_BLOCKLIST = [
22146
22169
  "**/.env",
22147
22170
  ".env",
@@ -22184,6 +22207,16 @@ function validateFilePath(config2, filePath) {
22184
22207
  if (!resolved.startsWith(normalizedRoot + import_node_path.default.sep) && resolved !== normalizedRoot) {
22185
22208
  return { safe: false, reason: `Path "${filePath}" resolves outside workspace root` };
22186
22209
  }
22210
+ if ((0, import_node_fs.existsSync)(resolved)) {
22211
+ try {
22212
+ const realResolved = (0, import_node_fs.realpathSync)(resolved);
22213
+ const realRoot = (0, import_node_fs.realpathSync)(normalizedRoot);
22214
+ if (!realResolved.startsWith(realRoot + import_node_path.default.sep) && realResolved !== realRoot) {
22215
+ return { safe: false, reason: `Path "${filePath}" resolves outside workspace via symlink` };
22216
+ }
22217
+ } catch {
22218
+ }
22219
+ }
22187
22220
  const relative = import_node_path.default.relative(normalizedRoot, resolved).replace(/\\/g, "/");
22188
22221
  if (matchesAny(config2.blocklist, relative)) {
22189
22222
  return { safe: false, reason: `Path "${filePath}" matches blocklist pattern` };
@@ -22272,6 +22305,26 @@ function computeMetrics(memos, impls, gates, checkpoints, artifacts, dependencie
22272
22305
  };
22273
22306
  }
22274
22307
 
22308
+ // src/file-mutex.ts
22309
+ var import_node_path2 = __toESM(require("node:path"));
22310
+ var locks = /* @__PURE__ */ new Map();
22311
+ async function withFileLock(filePath, fn) {
22312
+ const key = import_node_path2.default.resolve(filePath);
22313
+ while (locks.has(key)) {
22314
+ await locks.get(key);
22315
+ }
22316
+ let resolve2;
22317
+ locks.set(key, new Promise((r) => {
22318
+ resolve2 = r;
22319
+ }));
22320
+ try {
22321
+ return await fn();
22322
+ } finally {
22323
+ locks.delete(key);
22324
+ resolve2();
22325
+ }
22326
+ }
22327
+
22275
22328
  // src/tools.ts
22276
22329
  function computeLineHash(line) {
22277
22330
  let hash = 5381;
@@ -22281,6 +22334,17 @@ function computeLineHash(line) {
22281
22334
  return hash.toString(16).padStart(8, "0").slice(0, 8);
22282
22335
  }
22283
22336
  function registerTools(server2) {
22337
+ const safety = createFileSafety();
22338
+ function safeRead(file) {
22339
+ const check2 = validateFilePath(safety, file);
22340
+ if (!check2.safe) throw new Error(check2.reason);
22341
+ return readMarkdownFile(file);
22342
+ }
22343
+ function safeWrite(file, content) {
22344
+ const check2 = validateFilePath(safety, file);
22345
+ if (!check2.safe) throw new Error(check2.reason);
22346
+ writeMarkdownFile(file, content);
22347
+ }
22284
22348
  server2.tool(
22285
22349
  "create_checkpoint",
22286
22350
  "Create a review checkpoint in an annotated markdown file. Records current annotation counts and reviewed sections.",
@@ -22288,11 +22352,11 @@ function registerTools(server2) {
22288
22352
  file: external_exports.string().describe("Path to the annotated markdown file"),
22289
22353
  note: external_exports.string().describe('Checkpoint note (e.g., "Phase 1 review done")')
22290
22354
  },
22291
- async ({ file, note }) => {
22355
+ async ({ file, note }) => withFileLock(file, async () => {
22292
22356
  try {
22293
- const markdown = readMarkdownFile(file);
22357
+ const markdown = safeRead(file);
22294
22358
  const { checkpoint, updatedMarkdown } = createCheckpoint(markdown, note);
22295
- writeMarkdownFile(file, updatedMarkdown);
22359
+ safeWrite(file, updatedMarkdown);
22296
22360
  return {
22297
22361
  content: [{
22298
22362
  type: "text",
@@ -22308,7 +22372,7 @@ function registerTools(server2) {
22308
22372
  isError: true
22309
22373
  };
22310
22374
  }
22311
- }
22375
+ })
22312
22376
  );
22313
22377
  server2.tool(
22314
22378
  "get_checkpoints",
@@ -22318,7 +22382,7 @@ function registerTools(server2) {
22318
22382
  },
22319
22383
  async ({ file }) => {
22320
22384
  try {
22321
- const markdown = readMarkdownFile(file);
22385
+ const markdown = safeRead(file);
22322
22386
  const checkpoints = extractCheckpoints(markdown);
22323
22387
  return {
22324
22388
  content: [{
@@ -22346,7 +22410,7 @@ function registerTools(server2) {
22346
22410
  },
22347
22411
  async ({ file, target }) => {
22348
22412
  try {
22349
- const markdown = readMarkdownFile(file);
22413
+ const markdown = safeRead(file);
22350
22414
  const doc = buildHandoffDocument(markdown, file);
22351
22415
  const handoff = formatHandoffMarkdown(doc, target || "standalone");
22352
22416
  return {
@@ -22374,7 +22438,7 @@ function registerTools(server2) {
22374
22438
  },
22375
22439
  async ({ file }) => {
22376
22440
  try {
22377
- const markdown = readMarkdownFile(file);
22441
+ const markdown = safeRead(file);
22378
22442
  const counts = getAnnotationCounts(markdown);
22379
22443
  const checkpoints = extractCheckpoints(markdown);
22380
22444
  const sections = getSectionsWithAnnotations(markdown);
@@ -22410,7 +22474,7 @@ function registerTools(server2) {
22410
22474
  },
22411
22475
  async ({ file }) => {
22412
22476
  try {
22413
- const markdown = readMarkdownFile(file);
22477
+ const markdown = safeRead(file);
22414
22478
  const doc = parseHandoffFile(markdown);
22415
22479
  if (!doc) {
22416
22480
  return {
@@ -22446,7 +22510,7 @@ function registerTools(server2) {
22446
22510
  },
22447
22511
  async ({ file }) => {
22448
22512
  try {
22449
- const markdown = readMarkdownFile(file);
22513
+ const markdown = safeRead(file);
22450
22514
  const parts = splitDocument(markdown);
22451
22515
  const annotations = parts.memos.map((m) => ({
22452
22516
  id: m.id,
@@ -22486,13 +22550,14 @@ function registerTools(server2) {
22486
22550
  },
22487
22551
  async ({ file }) => {
22488
22552
  try {
22489
- const markdown = readMarkdownFile(file);
22553
+ const markdown = safeRead(file);
22490
22554
  const parts = splitDocument(markdown);
22491
22555
  const allSections = getAllSections(markdown);
22492
22556
  const reviewedSections = getSectionsWithAnnotations(markdown);
22493
22557
  const gates = evaluateAllGates(parts.gates, parts.memos);
22494
22558
  const open = parts.memos.filter((m) => m.status === "open").length;
22495
22559
  const inProgress = parts.memos.filter((m) => m.status === "in_progress").length;
22560
+ const needsReview = parts.memos.filter((m) => m.status === "needs_review").length;
22496
22561
  const answered = parts.memos.filter((m) => m.status === "answered").length;
22497
22562
  const done = parts.memos.filter((m) => m.status === "done").length;
22498
22563
  const failed = parts.memos.filter((m) => m.status === "failed").length;
@@ -22518,6 +22583,7 @@ function registerTools(server2) {
22518
22583
  total: parts.memos.length,
22519
22584
  open,
22520
22585
  inProgress,
22586
+ needsReview,
22521
22587
  answered,
22522
22588
  done,
22523
22589
  failed,
@@ -22563,29 +22629,59 @@ function registerTools(server2) {
22563
22629
  text: external_exports.string().describe("The review feedback or note to attach"),
22564
22630
  occurrence: external_exports.number().int().min(1).optional().describe("Which occurrence of anchorText to annotate (1-indexed, default 1). Use when the same text appears multiple times.")
22565
22631
  },
22566
- async ({ file, anchorText, type, text, occurrence }) => {
22632
+ async ({ file, anchorText, type, text, occurrence }) => withFileLock(file, async () => {
22567
22633
  try {
22568
- const markdown = readMarkdownFile(file);
22634
+ const markdown = safeRead(file);
22569
22635
  const parts = splitDocument(markdown);
22570
- const targetOccurrence = occurrence ?? 1;
22571
22636
  const bodyLines = parts.body.split("\n");
22572
22637
  let anchorLine = -1;
22573
- let matchCount = 0;
22574
- for (let i = 0; i < bodyLines.length; i++) {
22575
- if (bodyLines[i].includes(anchorText)) {
22576
- matchCount++;
22577
- if (matchCount === targetOccurrence) {
22578
- anchorLine = i;
22579
- break;
22638
+ if (occurrence) {
22639
+ let matchCount = 0;
22640
+ for (let i = 0; i < bodyLines.length; i++) {
22641
+ if (bodyLines[i].includes(anchorText)) {
22642
+ matchCount++;
22643
+ if (matchCount === occurrence) {
22644
+ anchorLine = i;
22645
+ break;
22646
+ }
22647
+ }
22648
+ }
22649
+ if (anchorLine === -1) {
22650
+ const errMsg = matchCount === 0 ? `Anchor text not found: "${anchorText}"` : `Anchor text "${anchorText}" has ${matchCount} occurrence(s), but occurrence=${occurrence} requested`;
22651
+ return {
22652
+ content: [{ type: "text", text: JSON.stringify({ error: errMsg }) }],
22653
+ isError: true
22654
+ };
22655
+ }
22656
+ } else {
22657
+ const matches = [];
22658
+ for (let i = 0; i < bodyLines.length; i++) {
22659
+ if (bodyLines[i].includes(anchorText)) matches.push(i);
22660
+ }
22661
+ if (matches.length === 0) {
22662
+ return {
22663
+ content: [{ type: "text", text: JSON.stringify({ error: `Anchor text not found: "${anchorText}"` }) }],
22664
+ isError: true
22665
+ };
22666
+ } else if (matches.length === 1) {
22667
+ anchorLine = matches[0];
22668
+ } else {
22669
+ const exactMatch = matches.find((i) => bodyLines[i].trim() === anchorText.trim());
22670
+ if (exactMatch !== void 0) {
22671
+ anchorLine = exactMatch;
22672
+ } else {
22673
+ let bestIdx = matches[0];
22674
+ let bestSurplus = Infinity;
22675
+ for (const idx of matches) {
22676
+ const surplus = bodyLines[idx].length - anchorText.length;
22677
+ if (surplus < bestSurplus) {
22678
+ bestSurplus = surplus;
22679
+ bestIdx = idx;
22680
+ }
22681
+ }
22682
+ anchorLine = bestIdx;
22580
22683
  }
22581
22684
  }
22582
- }
22583
- if (anchorLine === -1) {
22584
- const errMsg = matchCount === 0 ? `Anchor text not found: "${anchorText}"` : `Anchor text "${anchorText}" has ${matchCount} occurrence(s), but occurrence=${targetOccurrence} requested`;
22585
- return {
22586
- content: [{ type: "text", text: JSON.stringify({ error: errMsg }) }],
22587
- isError: true
22588
- };
22589
22685
  }
22590
22686
  const lineHash = computeLineHash(bodyLines[anchorLine]);
22591
22687
  const lineNum = anchorLine + 1;
@@ -22624,7 +22720,7 @@ function registerTools(server2) {
22624
22720
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22625
22721
  };
22626
22722
  const updated = mergeDocument(parts);
22627
- writeMarkdownFile(file, updated);
22723
+ safeWrite(file, updated);
22628
22724
  return {
22629
22725
  content: [{
22630
22726
  type: "text",
@@ -22640,7 +22736,7 @@ function registerTools(server2) {
22640
22736
  isError: true
22641
22737
  };
22642
22738
  }
22643
- }
22739
+ })
22644
22740
  );
22645
22741
  server2.tool(
22646
22742
  "respond_to_memo",
@@ -22650,9 +22746,9 @@ function registerTools(server2) {
22650
22746
  memoId: external_exports.string().describe("The memo ID to respond to"),
22651
22747
  response: external_exports.string().describe("The response text (markdown supported)")
22652
22748
  },
22653
- async ({ file, memoId, response }) => {
22749
+ async ({ file, memoId, response }) => withFileLock(file, async () => {
22654
22750
  try {
22655
- const markdown = readMarkdownFile(file);
22751
+ const markdown = safeRead(file);
22656
22752
  const parts = splitDocument(markdown);
22657
22753
  const memo = parts.memos.find((m) => m.id === memoId);
22658
22754
  if (!memo) {
@@ -22733,7 +22829,7 @@ function registerTools(server2) {
22733
22829
  parts.gates = evaluateAllGates(parts.gates, parts.memos);
22734
22830
  }
22735
22831
  const updated = mergeDocument(parts);
22736
- writeMarkdownFile(file, updated);
22832
+ safeWrite(file, updated);
22737
22833
  return {
22738
22834
  content: [{
22739
22835
  type: "text",
@@ -22754,7 +22850,7 @@ function registerTools(server2) {
22754
22850
  isError: true
22755
22851
  };
22756
22852
  }
22757
- }
22853
+ })
22758
22854
  );
22759
22855
  server2.tool(
22760
22856
  "update_memo_status",
@@ -22762,12 +22858,12 @@ function registerTools(server2) {
22762
22858
  {
22763
22859
  file: external_exports.string().describe("Path to the annotated markdown file"),
22764
22860
  memoId: external_exports.string().describe("The memo ID to update"),
22765
- status: external_exports.enum(["open", "in_progress", "answered", "done", "failed", "wontfix"]).describe("New status"),
22861
+ status: external_exports.enum(["open", "in_progress", "needs_review", "answered", "done", "failed", "wontfix"]).describe("New status"),
22766
22862
  owner: external_exports.enum(["human", "agent", "tool"]).optional().describe("Optionally change the owner")
22767
22863
  },
22768
- async ({ file, memoId, status, owner }) => {
22864
+ async ({ file, memoId, status, owner }) => withFileLock(file, async () => {
22769
22865
  try {
22770
- const markdown = readMarkdownFile(file);
22866
+ const markdown = safeRead(file);
22771
22867
  const parts = splitDocument(markdown);
22772
22868
  const memo = parts.memos.find((m) => m.id === memoId);
22773
22869
  if (!memo) {
@@ -22804,7 +22900,7 @@ function registerTools(server2) {
22804
22900
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22805
22901
  };
22806
22902
  const updated = mergeDocument(parts);
22807
- writeMarkdownFile(file, updated);
22903
+ safeWrite(file, updated);
22808
22904
  return {
22809
22905
  content: [{
22810
22906
  type: "text",
@@ -22820,7 +22916,7 @@ function registerTools(server2) {
22820
22916
  isError: true
22821
22917
  };
22822
22918
  }
22823
- }
22919
+ })
22824
22920
  );
22825
22921
  server2.tool(
22826
22922
  "update_cursor",
@@ -22831,9 +22927,9 @@ function registerTools(server2) {
22831
22927
  step: external_exports.string().describe('Current step (e.g., "3/7" or "Phase 2")'),
22832
22928
  nextAction: external_exports.string().describe("Description of the next action to take")
22833
22929
  },
22834
- async ({ file, taskId, step, nextAction }) => {
22930
+ async ({ file, taskId, step, nextAction }) => withFileLock(file, async () => {
22835
22931
  try {
22836
- const markdown = readMarkdownFile(file);
22932
+ const markdown = safeRead(file);
22837
22933
  const parts = splitDocument(markdown);
22838
22934
  if (parts.memos.length > 0 && !parts.memos.some((m) => m.id === taskId)) {
22839
22935
  return {
@@ -22854,7 +22950,7 @@ function registerTools(server2) {
22854
22950
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22855
22951
  };
22856
22952
  const updated = mergeDocument(parts);
22857
- writeMarkdownFile(file, updated);
22953
+ safeWrite(file, updated);
22858
22954
  return {
22859
22955
  content: [{
22860
22956
  type: "text",
@@ -22870,7 +22966,7 @@ function registerTools(server2) {
22870
22966
  isError: true
22871
22967
  };
22872
22968
  }
22873
- }
22969
+ })
22874
22970
  );
22875
22971
  server2.tool(
22876
22972
  "evaluate_gates",
@@ -22880,7 +22976,7 @@ function registerTools(server2) {
22880
22976
  },
22881
22977
  async ({ file }) => {
22882
22978
  try {
22883
- const markdown = readMarkdownFile(file);
22979
+ const markdown = safeRead(file);
22884
22980
  const parts = splitDocument(markdown);
22885
22981
  const gates = evaluateAllGates(parts.gates, parts.memos);
22886
22982
  return {
@@ -22917,7 +23013,7 @@ function registerTools(server2) {
22917
23013
  },
22918
23014
  async ({ file, target }) => {
22919
23015
  try {
22920
- const markdown = readMarkdownFile(file);
23016
+ const markdown = safeRead(file);
22921
23017
  if (target === "handoff") {
22922
23018
  const doc = buildHandoffDocument(markdown, file);
22923
23019
  const handoff = formatHandoffMarkdown(doc, "standalone");
@@ -22955,7 +23051,7 @@ function registerTools(server2) {
22955
23051
  );
22956
23052
  server2.tool(
22957
23053
  "apply_memo",
22958
- "Apply an implementation action to a memo. Supports text_replace (on current document), file_patch (apply patch to target file), and file_create (create a new file). Creates a snapshot before modification, records the implementation, and updates memo status to done.",
23054
+ "Apply an implementation action to a memo. Supports text_replace (replaces all occurrences in current document), file_patch (overwrites target file \u2014 snapshot saved first), and file_create (create a new file). Creates a snapshot before modification, records the implementation, and updates memo status to done.",
22959
23055
  {
22960
23056
  file: external_exports.string().describe("Path to the annotated markdown file"),
22961
23057
  memoId: external_exports.string().describe("The memo ID to apply implementation to"),
@@ -22967,9 +23063,9 @@ function registerTools(server2) {
22967
23063
  patch: external_exports.string().optional().describe("For file_patch: the patch content"),
22968
23064
  content: external_exports.string().optional().describe("For file_create: the file content to write")
22969
23065
  },
22970
- async ({ file, memoId, action, dryRun, oldText, newText, targetFile, patch, content: fileContent }) => {
23066
+ async ({ file, memoId, action, dryRun, oldText, newText, targetFile, patch, content: fileContent }) => withFileLock(file, async () => {
22971
23067
  try {
22972
- const markdown = readMarkdownFile(file);
23068
+ const markdown = safeRead(file);
22973
23069
  const parts = splitDocument(markdown);
22974
23070
  const memo = parts.memos.find((m) => m.id === memoId);
22975
23071
  if (!memo) {
@@ -23020,16 +23116,6 @@ function registerTools(server2) {
23020
23116
  }]
23021
23117
  };
23022
23118
  }
23023
- if ((action === "file_patch" || action === "file_create") && targetFile) {
23024
- const safety = createFileSafety();
23025
- const check2 = validateFilePath(safety, targetFile);
23026
- if (!check2.safe) {
23027
- return {
23028
- content: [{ type: "text", text: JSON.stringify({ error: `File safety: ${check2.reason}` }) }],
23029
- isError: true
23030
- };
23031
- }
23032
- }
23033
23119
  writeSnapshot(file, markdown);
23034
23120
  if (action === "text_replace") {
23035
23121
  if (!parts.body.includes(oldText)) {
@@ -23038,14 +23124,17 @@ function registerTools(server2) {
23038
23124
  isError: true
23039
23125
  };
23040
23126
  }
23041
- parts.body = parts.body.replace(oldText, newText);
23127
+ parts.body = parts.body.split(oldText).join(newText);
23042
23128
  } else if (action === "file_patch") {
23043
- writeMarkdownFile(targetFile, patch);
23129
+ if ((0, import_node_fs2.existsSync)(targetFile)) {
23130
+ writeSnapshot(targetFile, readMarkdownFile(targetFile));
23131
+ }
23132
+ safeWrite(targetFile, patch);
23044
23133
  } else if (action === "file_create") {
23045
- writeMarkdownFile(targetFile, fileContent);
23134
+ safeWrite(targetFile, fileContent);
23046
23135
  }
23047
23136
  parts.impls.push(impl);
23048
- memo.status = "done";
23137
+ memo.status = "needs_review";
23049
23138
  memo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
23050
23139
  if (parts.gates.length > 0) {
23051
23140
  parts.gates = evaluateAllGates(parts.gates, parts.memos);
@@ -23060,7 +23149,7 @@ function registerTools(server2) {
23060
23149
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
23061
23150
  };
23062
23151
  const updated = mergeDocument(parts);
23063
- writeMarkdownFile(file, updated);
23152
+ safeWrite(file, updated);
23064
23153
  return {
23065
23154
  content: [{
23066
23155
  type: "text",
@@ -23076,7 +23165,7 @@ function registerTools(server2) {
23076
23165
  isError: true
23077
23166
  };
23078
23167
  }
23079
- }
23168
+ })
23080
23169
  );
23081
23170
  server2.tool(
23082
23171
  "link_artifacts",
@@ -23086,9 +23175,9 @@ function registerTools(server2) {
23086
23175
  memoId: external_exports.string().describe("The memo ID to link artifacts to"),
23087
23176
  files: external_exports.array(external_exports.string()).describe("Array of relative file paths to link")
23088
23177
  },
23089
- async ({ file, memoId, files: artifactFiles }) => {
23178
+ async ({ file, memoId, files: artifactFiles }) => withFileLock(file, async () => {
23090
23179
  try {
23091
- const markdown = readMarkdownFile(file);
23180
+ const markdown = safeRead(file);
23092
23181
  const parts = splitDocument(markdown);
23093
23182
  const memo = parts.memos.find((m) => m.id === memoId);
23094
23183
  if (!memo) {
@@ -23105,7 +23194,7 @@ function registerTools(server2) {
23105
23194
  };
23106
23195
  parts.artifacts.push(artifact);
23107
23196
  const updated = mergeDocument(parts);
23108
- writeMarkdownFile(file, updated);
23197
+ safeWrite(file, updated);
23109
23198
  return {
23110
23199
  content: [{
23111
23200
  type: "text",
@@ -23121,7 +23210,7 @@ function registerTools(server2) {
23121
23210
  isError: true
23122
23211
  };
23123
23212
  }
23124
- }
23213
+ })
23125
23214
  );
23126
23215
  server2.tool(
23127
23216
  "update_memo_progress",
@@ -23129,12 +23218,12 @@ function registerTools(server2) {
23129
23218
  {
23130
23219
  file: external_exports.string().describe("Path to the annotated markdown file"),
23131
23220
  memoId: external_exports.string().describe("The memo ID to update progress for"),
23132
- status: external_exports.enum(["in_progress", "done", "failed"]).describe("New progress status"),
23221
+ status: external_exports.enum(["in_progress", "needs_review", "done", "failed"]).describe("New progress status"),
23133
23222
  message: external_exports.string().describe("Progress message describing what was done or what failed")
23134
23223
  },
23135
- async ({ file, memoId, status, message }) => {
23224
+ async ({ file, memoId, status, message }) => withFileLock(file, async () => {
23136
23225
  try {
23137
- const markdown = readMarkdownFile(file);
23226
+ const markdown = safeRead(file);
23138
23227
  const parts = splitDocument(markdown);
23139
23228
  const memo = parts.memos.find((m) => m.id === memoId);
23140
23229
  if (!memo) {
@@ -23165,7 +23254,7 @@ function registerTools(server2) {
23165
23254
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
23166
23255
  };
23167
23256
  const updated = mergeDocument(parts);
23168
- writeMarkdownFile(file, updated);
23257
+ safeWrite(file, updated);
23169
23258
  return {
23170
23259
  content: [{
23171
23260
  type: "text",
@@ -23181,7 +23270,7 @@ function registerTools(server2) {
23181
23270
  isError: true
23182
23271
  };
23183
23272
  }
23184
- }
23273
+ })
23185
23274
  );
23186
23275
  server2.tool(
23187
23276
  "rollback_memo",
@@ -23190,9 +23279,9 @@ function registerTools(server2) {
23190
23279
  file: external_exports.string().describe("Path to the annotated markdown file"),
23191
23280
  memoId: external_exports.string().describe("The memo ID to rollback")
23192
23281
  },
23193
- async ({ file, memoId }) => {
23282
+ async ({ file, memoId }) => withFileLock(file, async () => {
23194
23283
  try {
23195
- const markdown = readMarkdownFile(file);
23284
+ const markdown = safeRead(file);
23196
23285
  const parts = splitDocument(markdown);
23197
23286
  const memo = parts.memos.find((m) => m.id === memoId);
23198
23287
  if (!memo) {
@@ -23212,7 +23301,7 @@ function registerTools(server2) {
23212
23301
  for (const op of latestImpl.operations) {
23213
23302
  if (op.type === "text_replace") {
23214
23303
  if (parts.body.includes(op.after)) {
23215
- parts.body = parts.body.replace(op.after, op.before);
23304
+ parts.body = parts.body.split(op.after).join(op.before);
23216
23305
  }
23217
23306
  }
23218
23307
  }
@@ -23232,7 +23321,7 @@ function registerTools(server2) {
23232
23321
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
23233
23322
  };
23234
23323
  const updated = mergeDocument(parts);
23235
- writeMarkdownFile(file, updated);
23324
+ safeWrite(file, updated);
23236
23325
  return {
23237
23326
  content: [{
23238
23327
  type: "text",
@@ -23252,7 +23341,7 @@ function registerTools(server2) {
23252
23341
  isError: true
23253
23342
  };
23254
23343
  }
23255
- }
23344
+ })
23256
23345
  );
23257
23346
  server2.tool(
23258
23347
  "batch_apply",
@@ -23269,26 +23358,18 @@ function registerTools(server2) {
23269
23358
  content: external_exports.string().optional().describe("For file_create: the file content to write")
23270
23359
  })).describe("Array of operations to apply")
23271
23360
  },
23272
- async ({ file, operations }) => {
23361
+ async ({ file, operations }) => withFileLock(file, async () => {
23273
23362
  try {
23274
- const markdown = readMarkdownFile(file);
23363
+ const markdown = safeRead(file);
23275
23364
  const parts = splitDocument(markdown);
23276
23365
  writeSnapshot(file, markdown);
23277
23366
  const results = [];
23278
- const safety = createFileSafety();
23279
23367
  for (const op of operations) {
23280
23368
  const memo = parts.memos.find((m) => m.id === op.memoId);
23281
23369
  if (!memo) {
23282
23370
  results.push({ memoId: op.memoId, implId: "", status: `error: memo not found` });
23283
23371
  continue;
23284
23372
  }
23285
- if ((op.action === "file_patch" || op.action === "file_create") && op.targetFile) {
23286
- const check2 = validateFilePath(safety, op.targetFile);
23287
- if (!check2.safe) {
23288
- results.push({ memoId: op.memoId, implId: "", status: `error: file safety: ${check2.reason}` });
23289
- continue;
23290
- }
23291
- }
23292
23373
  let operation;
23293
23374
  if (op.action === "text_replace") {
23294
23375
  if (!op.oldText || op.newText === void 0) {
@@ -23300,21 +23381,24 @@ function registerTools(server2) {
23300
23381
  results.push({ memoId: op.memoId, implId: "", status: "error: oldText not found in body" });
23301
23382
  continue;
23302
23383
  }
23303
- parts.body = parts.body.replace(op.oldText, op.newText);
23384
+ parts.body = parts.body.split(op.oldText).join(op.newText);
23304
23385
  } else if (op.action === "file_patch") {
23305
23386
  if (!op.targetFile || !op.patch) {
23306
23387
  results.push({ memoId: op.memoId, implId: "", status: "error: file_patch requires targetFile and patch" });
23307
23388
  continue;
23308
23389
  }
23309
23390
  operation = { type: "file_patch", file: op.targetFile, patch: op.patch };
23310
- writeMarkdownFile(op.targetFile, op.patch);
23391
+ if ((0, import_node_fs2.existsSync)(op.targetFile)) {
23392
+ writeSnapshot(op.targetFile, readMarkdownFile(op.targetFile));
23393
+ }
23394
+ safeWrite(op.targetFile, op.patch);
23311
23395
  } else {
23312
23396
  if (!op.targetFile || op.content === void 0) {
23313
23397
  results.push({ memoId: op.memoId, implId: "", status: "error: file_create requires targetFile and content" });
23314
23398
  continue;
23315
23399
  }
23316
23400
  operation = { type: "file_create", file: op.targetFile, content: op.content };
23317
- writeMarkdownFile(op.targetFile, op.content);
23401
+ safeWrite(op.targetFile, op.content);
23318
23402
  }
23319
23403
  const impl = {
23320
23404
  id: `impl_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`,
@@ -23325,7 +23409,7 @@ function registerTools(server2) {
23325
23409
  appliedAt: (/* @__PURE__ */ new Date()).toISOString()
23326
23410
  };
23327
23411
  parts.impls.push(impl);
23328
- memo.status = "done";
23412
+ memo.status = "needs_review";
23329
23413
  memo.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
23330
23414
  results.push({ memoId: op.memoId, implId: impl.id, status: "applied" });
23331
23415
  }
@@ -23343,7 +23427,7 @@ function registerTools(server2) {
23343
23427
  };
23344
23428
  writeTransaction(file, { type: "batch_apply", results, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
23345
23429
  const updated = mergeDocument(parts);
23346
- writeMarkdownFile(file, updated);
23430
+ safeWrite(file, updated);
23347
23431
  return {
23348
23432
  content: [{
23349
23433
  type: "text",
@@ -23359,7 +23443,7 @@ function registerTools(server2) {
23359
23443
  isError: true
23360
23444
  };
23361
23445
  }
23362
- }
23446
+ })
23363
23447
  );
23364
23448
  server2.tool(
23365
23449
  "get_memo_changes",
@@ -23370,7 +23454,7 @@ function registerTools(server2) {
23370
23454
  },
23371
23455
  async ({ file, memoId }) => {
23372
23456
  try {
23373
- const markdown = readMarkdownFile(file);
23457
+ const markdown = safeRead(file);
23374
23458
  const parts = splitDocument(markdown);
23375
23459
  const impls = memoId ? parts.impls.filter((imp) => imp.memoId === memoId) : parts.impls;
23376
23460
  const allProgress = readProgress(file);
@@ -23401,13 +23485,13 @@ function log(msg) {
23401
23485
  }
23402
23486
  var server = new McpServer({
23403
23487
  name: "md-feedback",
23404
- version: "1.1.0"
23488
+ version: "1.2.1"
23405
23489
  });
23406
23490
  registerTools(server);
23407
23491
  async function main() {
23408
23492
  const transport = new StdioServerTransport();
23409
23493
  await server.connect(transport);
23410
- log(`v${"1.1.0"} ready (stdio)`);
23494
+ log(`v${"1.2.1"} ready (stdio)`);
23411
23495
  }
23412
23496
  main().catch((err) => {
23413
23497
  log(`fatal: ${err}`);
package/package.json CHANGED
@@ -1,70 +1,73 @@
1
- {
2
- "name": "md-feedback",
3
- "version": "1.1.0",
4
- "description": "MCP server for markdown plan review — AI agents read annotations, mark tasks done, evaluate quality gates, and generate session handoffs. 19 tools for Claude Code, Cursor, Copilot, and 8 more AI tools.",
5
- "license": "SUL-1.0",
6
- "author": "Yeomin Seon",
7
- "type": "commonjs",
8
- "bin": {
9
- "md-feedback": "./bin/md-feedback.cjs"
10
- },
11
- "files": [
12
- "dist/mcp-server.js",
13
- "bin/md-feedback.cjs",
14
- "README.md"
15
- ],
16
- "scripts": {
17
- "build": "node esbuild.mjs"
18
- },
19
- "dependencies": {
20
- "@modelcontextprotocol/sdk": "^1.12.0",
21
- "zod": "^3.23.0"
22
- },
23
- "devDependencies": {
24
- "esbuild": "^0.24.2",
25
- "typescript": "^5.7.0"
26
- },
27
- "engines": {
28
- "node": ">=18"
29
- },
30
- "repository": {
31
- "type": "git",
32
- "url": "https://github.com/yeominux/md-feedback"
33
- },
34
- "bugs": {
35
- "url": "https://github.com/yeominux/md-feedback/issues"
36
- },
37
- "homepage": "https://github.com/yeominux/md-feedback#mcp-server",
38
- "keywords": [
39
- "mcp",
40
- "mcp-server",
41
- "model-context-protocol",
42
- "markdown",
43
- "feedback",
44
- "ai",
45
- "annotation",
46
- "review",
47
- "plan-review",
48
- "ai-agent",
49
- "coding-workflow",
50
- "handoff",
51
- "session-handoff",
52
- "structured-feedback",
53
- "checkpoint",
54
- "ai-context",
55
- "ai-coding",
56
- "claude-code",
57
- "cursor-ai",
58
- "vibe-coding",
59
- "context-engineering",
60
- "gates",
61
- "plan-review-tool",
62
- "document-annotation",
63
- "code-review",
64
- "ai-workflow",
65
- "copilot",
66
- "markdown-review",
67
- "developer-tools",
68
- "quality-gate"
69
- ]
70
- }
1
+ {
2
+ "name": "md-feedback",
3
+ "version": "1.2.1",
4
+ "description": "MCP server for markdown plan review — AI agents read annotations, mark tasks done, evaluate quality gates, and generate session handoffs. 19 tools for Claude Code, Cursor, Copilot, and 8 more AI tools.",
5
+ "license": "SUL-1.0",
6
+ "author": "Yeomin Seon",
7
+ "type": "commonjs",
8
+ "bin": {
9
+ "md-feedback": "./bin/md-feedback.cjs"
10
+ },
11
+ "files": [
12
+ "dist/mcp-server.js",
13
+ "bin/md-feedback.cjs",
14
+ "README.md"
15
+ ],
16
+ "scripts": {
17
+ "build": "node esbuild.mjs"
18
+ },
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.12.0",
21
+ "zod": "^3.23.0"
22
+ },
23
+ "devDependencies": {
24
+ "esbuild": "^0.24.2",
25
+ "typescript": "^5.7.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/yeominux/md-feedback"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/yeominux/md-feedback/issues"
36
+ },
37
+ "homepage": "https://github.com/yeominux/md-feedback#mcp-server",
38
+ "keywords": [
39
+ "mcp",
40
+ "mcp-server",
41
+ "model-context-protocol",
42
+ "markdown",
43
+ "feedback",
44
+ "ai",
45
+ "annotation",
46
+ "review",
47
+ "plan-review",
48
+ "ai-agent",
49
+ "coding-workflow",
50
+ "handoff",
51
+ "session-handoff",
52
+ "structured-feedback",
53
+ "checkpoint",
54
+ "ai-context",
55
+ "ai-coding",
56
+ "claude-code",
57
+ "cursor-ai",
58
+ "vibe-coding",
59
+ "context-engineering",
60
+ "gates",
61
+ "plan-review-tool",
62
+ "document-annotation",
63
+ "code-review",
64
+ "ai-workflow",
65
+ "copilot",
66
+ "markdown-review",
67
+ "developer-tools",
68
+ "quality-gate",
69
+ "file-safety",
70
+ "concurrent-safety",
71
+ "gate-override"
72
+ ]
73
+ }