ronds-mcp-tracker 0.1.7 → 0.1.9

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 +88 -37
  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
  }
@@ -171,6 +189,24 @@ function loadSnapshot(snapshotId) {
171
189
  const raw = fs.readFileSync(filePath, 'utf8');
172
190
  return JSON.parse(raw);
173
191
  }
192
+ function loadLatestSnapshotForFile(filePath) {
193
+ const dir = ensureSnapshotsDirExists();
194
+ const snapshots = fs.readdirSync(dir)
195
+ .map((name) => path.join(dir, name))
196
+ .filter((snapshotPath) => fs.statSync(snapshotPath).isFile())
197
+ .map((snapshotPath) => {
198
+ try {
199
+ const raw = fs.readFileSync(snapshotPath, 'utf8');
200
+ return JSON.parse(raw);
201
+ }
202
+ catch {
203
+ return null;
204
+ }
205
+ })
206
+ .filter((snapshot) => snapshot !== null && snapshot.file_path === filePath)
207
+ .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
208
+ return snapshots[0] ?? null;
209
+ }
174
210
  function deleteSnapshot(snapshotId) {
175
211
  const filePath = getSnapshotPath(snapshotId);
176
212
  if (fs.existsSync(filePath)) {
@@ -259,7 +295,7 @@ function getFailedEventPath(eventId) {
259
295
  }
260
296
  function handleBeforeEdit(input) {
261
297
  const filePath = validateFilePath(input.file_path);
262
- const content = readFileContent(filePath);
298
+ const content = fs.existsSync(filePath) ? readFileContent(filePath) : '';
263
299
  const snapshotId = randomUUID();
264
300
  saveSnapshot({
265
301
  snapshot_id: snapshotId,
@@ -279,55 +315,54 @@ async function handleAfterEdit(input, config) {
279
315
  if (!snapshotId) {
280
316
  throw new Error('snapshot_id is required');
281
317
  }
282
- const snapshot = loadSnapshot(snapshotId);
318
+ const snapshot = loadSnapshot(snapshotId) ?? loadLatestSnapshotForFile(filePath);
283
319
  if (!snapshot) {
284
- return JSON.stringify({
285
- status: 'snapshot_not_found',
286
- snapshot_id: snapshotId,
287
- file_path: filePath
288
- });
320
+ return JSON.stringify({ success: false });
289
321
  }
290
322
  if (snapshot.file_path !== filePath) {
291
- return JSON.stringify({
292
- status: 'snapshot_file_path_mismatch',
293
- snapshot_id: snapshotId,
294
- file_path: filePath,
295
- snapshot_file_path: snapshot.file_path
296
- });
323
+ return JSON.stringify({ success: false });
297
324
  }
298
325
  const oldStr = snapshot.content;
299
326
  const newStr = readFileContent(filePath);
300
327
  const addedLines = getAddedLines(oldStr, newStr);
301
328
  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
- };
329
+ const event = await buildChangeEvent({
330
+ filePath,
331
+ operation: 'edit',
332
+ addedContent: addedLines,
333
+ added: diffStats.added,
334
+ removed: diffStats.removed,
335
+ config
336
+ });
315
337
  await publishToKafka(config, event);
316
338
  if (fs.existsSync(getFailedEventPath(event.event_id))) {
317
- return JSON.stringify({
318
- status: 'queued_for_retry',
319
- event_id: event.event_id,
320
- snapshot_id: snapshotId,
321
- lines_changed: diffStats.added + diffStats.removed,
322
- added_content: addedLines
323
- });
339
+ return JSON.stringify({ success: false });
324
340
  }
325
341
  deleteSnapshot(snapshotId);
342
+ return JSON.stringify({ success: true });
343
+ }
344
+ async function handleDirectAdd(input, config) {
345
+ const filePath = validateFilePath(input.file_path);
346
+ const addedContent = input.added_content;
347
+ if (typeof addedContent !== 'string' || !addedContent.trim()) {
348
+ throw new Error('added_content is required');
349
+ }
350
+ const added = countNonEmptyLines(addedContent);
351
+ const event = await buildChangeEvent({
352
+ filePath,
353
+ operation: 'add',
354
+ addedContent,
355
+ added,
356
+ removed: 0,
357
+ config
358
+ });
359
+ await publishToKafka(config, event);
360
+ const status = fs.existsSync(getFailedEventPath(event.event_id)) ? 'queued_for_retry' : 'recorded';
326
361
  return JSON.stringify({
327
- status: 'recorded',
362
+ status,
328
363
  event_id: event.event_id,
329
- lines_changed: diffStats.added + diffStats.removed,
330
- added_content: addedLines
364
+ lines_changed: added,
365
+ added_content: addedContent
331
366
  });
332
367
  }
333
368
  function handleCleanupFailed(input) {
@@ -363,6 +398,18 @@ const AFTER_EDIT_TOOL_SCHEMA = {
363
398
  required: ['file_path', 'snapshot_id']
364
399
  }
365
400
  };
401
+ const DIRECT_ADD_TOOL_SCHEMA = {
402
+ name: 'direct_add',
403
+ description: 'Record added content directly when no pre-edit snapshot is available.',
404
+ inputSchema: {
405
+ type: 'object',
406
+ properties: {
407
+ file_path: { type: 'string' },
408
+ added_content: { type: 'string' }
409
+ },
410
+ required: ['file_path', 'added_content']
411
+ }
412
+ };
366
413
  const CLEANUP_FAILED_TOOL_SCHEMA = {
367
414
  name: 'cleanup_failed',
368
415
  description: 'Clean up failed queue files by age.',
@@ -396,7 +443,7 @@ async function main() {
396
443
  }
397
444
  });
398
445
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
399
- tools: [BEFORE_EDIT_TOOL_SCHEMA, AFTER_EDIT_TOOL_SCHEMA, CLEANUP_FAILED_TOOL_SCHEMA, REPLAY_FAILED_TOOL_SCHEMA]
446
+ tools: [BEFORE_EDIT_TOOL_SCHEMA, AFTER_EDIT_TOOL_SCHEMA, DIRECT_ADD_TOOL_SCHEMA, CLEANUP_FAILED_TOOL_SCHEMA, REPLAY_FAILED_TOOL_SCHEMA]
400
447
  }));
401
448
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
402
449
  const name = request.params.name;
@@ -409,6 +456,10 @@ async function main() {
409
456
  const result = await handleAfterEdit(input, config);
410
457
  return { content: [{ type: 'text', text: result }] };
411
458
  }
459
+ if (name === 'direct_add') {
460
+ const result = await handleDirectAdd(input, config);
461
+ return { content: [{ type: 'text', text: result }] };
462
+ }
412
463
  if (name === 'cleanup_failed') {
413
464
  const result = handleCleanupFailed(input);
414
465
  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.9",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "ronds-mcp-tracker": "./bin/cli.js"