ronds-mcp-tracker 0.1.7 → 0.1.8

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 +61 -4
  2. package/dist/server.js +68 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -47,18 +47,43 @@ Custom worker id:
47
47
  - Kafka config is currently hardcoded (see `src/config.ts`) and can be extended later.
48
48
 
49
49
  ## Tools
50
+ ### `before_edit`
51
+ 适用场景:支持在编辑前先调用 MCP 的客户端。服务端会保存编辑前快照,并返回 `snapshot_id` 供后续 `after_edit` 使用。
52
+
53
+ Input:
54
+ - `file_path`: file path
55
+
56
+ Output:
57
+ - `status`: `snapshotted`
58
+ - `snapshot_id`: snapshot id
59
+ - `file_path`: normalized file path
60
+
50
61
  ### `after_edit`
62
+ 适用场景:基于 `before_edit` 保存的快照,在编辑后计算完整 diff。该模式能区分新增与删除,`file.operation` 为 `edit`。
63
+
51
64
  Input:
52
65
  - `file_path`: file path
53
- - `old_string`: previous content
54
- - `new_string`: new content
66
+ - `snapshot_id`: snapshot id returned by `before_edit`
55
67
 
56
68
  Output:
57
- - `status`: `recorded`
69
+ - `status`: `recorded` or `queued_for_retry`
58
70
  - `event_id`: event id
59
- - `lines_changed`: total lines changed
71
+ - `lines_changed`: total changed lines (`added + removed`)
60
72
  - `added_content`: added lines content (may be truncated)
61
73
 
74
+ ### `direct_add`
75
+ 适用场景:客户端无法在编辑前调用 MCP,只能在编辑完成后上报新增内容时使用。该模式不依赖 snapshot,直接记录新增内容,`file.operation` 为 `add`。
76
+
77
+ Input:
78
+ - `file_path`: file path
79
+ - `added_content`: newly added content supplied by the client
80
+
81
+ Output:
82
+ - `status`: `recorded` or `queued_for_retry`
83
+ - `event_id`: event id
84
+ - `lines_changed`: non-empty line count in `added_content`
85
+ - `added_content`: original added content
86
+
62
87
  ### `cleanup_failed`
63
88
  Input:
64
89
  - `max_age_hours` (optional): default 168
@@ -79,6 +104,38 @@ Output:
79
104
  ## Failed Queue
80
105
  Failed events are stored in `~/.mcp-tracker/failed` as JSON files. Old files are cleaned by age and replayed in FIFO order.
81
106
 
107
+ ## Usage Flow
108
+ ### Snapshot mode (`before_edit` + `after_edit`)
109
+ 适用于 Claude Code 这类可以在编辑前后分别调用 MCP 的客户端:
110
+
111
+ 1. 调用 `before_edit` 保存服务端快照
112
+ 2. 完成文件编辑
113
+ 3. 调用 `after_edit` 并传入 `snapshot_id`
114
+
115
+ 说明:
116
+ - 如果目标文件在 `before_edit` 时还不存在,服务端会保存一个空快照。
117
+ - 随后 `after_edit` 会把该文件视为“从空内容新增”,正常上传新增内容。
118
+ - 这种场景下事件类型仍然是 `file.operation = edit`,用于保持 snapshot 模式的一致性。
119
+
120
+ ### Direct add mode (`direct_add`)
121
+ 适用于 Cursor 这类通常只能在编辑完成后调用 MCP 的客户端:
122
+
123
+ 1. 客户端拿到本次新增内容
124
+ 2. 调用 `direct_add(file_path, added_content)`
125
+ 3. 服务端直接构建 `ChangeEvent` 并发布到 Kafka
126
+
127
+ 适用建议:
128
+ - 能走 `before_edit` / `after_edit` 时,优先使用 snapshot 模式。
129
+ - 客户端只能后调用、拿不到编辑前快照时,再使用 `direct_add` 兜底。
130
+
131
+ ### Direct add mode (`direct_add`)
132
+ 适用于 Cursor 这类通常只能在编辑完成后调用 MCP 的客户端:
133
+
134
+ 1. 客户端拿到本次新增内容
135
+ 2. 调用 `direct_add(file_path, added_content)`
136
+ 3. 服务端直接构建 `ChangeEvent` 并发布到 Kafka
137
+
82
138
  ## Development
83
139
  - Build: `npm run build`
84
140
  - Test tools list: `echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/server.js`
141
+ - Test direct add locally: call `tools/call` with `direct_add`, `file_path`, and multi-line `added_content`
package/dist/server.js CHANGED
@@ -126,6 +126,24 @@ function countDiff(oldStr, newStr) {
126
126
  }
127
127
  return { added, removed };
128
128
  }
129
+ function countNonEmptyLines(content) {
130
+ return content.split('\n').filter((line) => line !== '').length;
131
+ }
132
+ async function buildChangeEvent(params) {
133
+ const gitInfo = await getGitInfo(process.cwd());
134
+ return {
135
+ event_id: randomUUID(),
136
+ timestamp: new Date().toISOString(),
137
+ worker_id: params.config.worker_id,
138
+ git: gitInfo ? { repo_name: gitInfo.repo_name } : null,
139
+ file: { path: getKafkaFilePath(params.filePath, gitInfo), operation: params.operation },
140
+ changes: {
141
+ added_content: params.addedContent,
142
+ added: params.added,
143
+ removed: params.removed
144
+ }
145
+ };
146
+ }
129
147
  function getFailedDir() {
130
148
  return path.join(os.homedir(), '.mcp-tracker', 'failed');
131
149
  }
@@ -259,7 +277,7 @@ function getFailedEventPath(eventId) {
259
277
  }
260
278
  function handleBeforeEdit(input) {
261
279
  const filePath = validateFilePath(input.file_path);
262
- const content = readFileContent(filePath);
280
+ const content = fs.existsSync(filePath) ? readFileContent(filePath) : '';
263
281
  const snapshotId = randomUUID();
264
282
  saveSnapshot({
265
283
  snapshot_id: snapshotId,
@@ -299,19 +317,14 @@ async function handleAfterEdit(input, config) {
299
317
  const newStr = readFileContent(filePath);
300
318
  const addedLines = getAddedLines(oldStr, newStr);
301
319
  const diffStats = countDiff(oldStr, newStr);
302
- const gitInfo = await getGitInfo(process.cwd());
303
- const event = {
304
- event_id: randomUUID(),
305
- timestamp: new Date().toISOString(),
306
- worker_id: config.worker_id,
307
- git: gitInfo ? { repo_name: gitInfo.repo_name } : null,
308
- file: { path: getKafkaFilePath(filePath, gitInfo), operation: 'edit' },
309
- changes: {
310
- added_content: addedLines,
311
- added: diffStats.added,
312
- removed: diffStats.removed
313
- }
314
- };
320
+ const event = await buildChangeEvent({
321
+ filePath,
322
+ operation: 'edit',
323
+ addedContent: addedLines,
324
+ added: diffStats.added,
325
+ removed: diffStats.removed,
326
+ config
327
+ });
315
328
  await publishToKafka(config, event);
316
329
  if (fs.existsSync(getFailedEventPath(event.event_id))) {
317
330
  return JSON.stringify({
@@ -330,6 +343,30 @@ async function handleAfterEdit(input, config) {
330
343
  added_content: addedLines
331
344
  });
332
345
  }
346
+ async function handleDirectAdd(input, config) {
347
+ const filePath = validateFilePath(input.file_path);
348
+ const addedContent = input.added_content;
349
+ if (typeof addedContent !== 'string' || !addedContent.trim()) {
350
+ throw new Error('added_content is required');
351
+ }
352
+ const added = countNonEmptyLines(addedContent);
353
+ const event = await buildChangeEvent({
354
+ filePath,
355
+ operation: 'add',
356
+ addedContent,
357
+ added,
358
+ removed: 0,
359
+ config
360
+ });
361
+ await publishToKafka(config, event);
362
+ const status = fs.existsSync(getFailedEventPath(event.event_id)) ? 'queued_for_retry' : 'recorded';
363
+ return JSON.stringify({
364
+ status,
365
+ event_id: event.event_id,
366
+ lines_changed: added,
367
+ added_content: addedContent
368
+ });
369
+ }
333
370
  function handleCleanupFailed(input) {
334
371
  const maxAgeHours = input.max_age_hours ?? 168;
335
372
  const result = cleanupFailed(maxAgeHours);
@@ -363,6 +400,18 @@ const AFTER_EDIT_TOOL_SCHEMA = {
363
400
  required: ['file_path', 'snapshot_id']
364
401
  }
365
402
  };
403
+ const DIRECT_ADD_TOOL_SCHEMA = {
404
+ name: 'direct_add',
405
+ description: 'Record added content directly when no pre-edit snapshot is available.',
406
+ inputSchema: {
407
+ type: 'object',
408
+ properties: {
409
+ file_path: { type: 'string' },
410
+ added_content: { type: 'string' }
411
+ },
412
+ required: ['file_path', 'added_content']
413
+ }
414
+ };
366
415
  const CLEANUP_FAILED_TOOL_SCHEMA = {
367
416
  name: 'cleanup_failed',
368
417
  description: 'Clean up failed queue files by age.',
@@ -396,7 +445,7 @@ async function main() {
396
445
  }
397
446
  });
398
447
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
399
- tools: [BEFORE_EDIT_TOOL_SCHEMA, AFTER_EDIT_TOOL_SCHEMA, CLEANUP_FAILED_TOOL_SCHEMA, REPLAY_FAILED_TOOL_SCHEMA]
448
+ tools: [BEFORE_EDIT_TOOL_SCHEMA, AFTER_EDIT_TOOL_SCHEMA, DIRECT_ADD_TOOL_SCHEMA, CLEANUP_FAILED_TOOL_SCHEMA, REPLAY_FAILED_TOOL_SCHEMA]
400
449
  }));
401
450
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
402
451
  const name = request.params.name;
@@ -409,6 +458,10 @@ async function main() {
409
458
  const result = await handleAfterEdit(input, config);
410
459
  return { content: [{ type: 'text', text: result }] };
411
460
  }
461
+ if (name === 'direct_add') {
462
+ const result = await handleDirectAdd(input, config);
463
+ return { content: [{ type: 'text', text: result }] };
464
+ }
412
465
  if (name === 'cleanup_failed') {
413
466
  const result = handleCleanupFailed(input);
414
467
  return { content: [{ type: 'text', text: result }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ronds-mcp-tracker",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ronds-mcp-tracker": "./bin/cli.js"