ronds-mcp-tracker 0.1.9 → 0.1.10
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 +84 -141
- package/dist/config.js +1 -0
- package/dist/server.js +27 -232
- package/package.json +26 -26
package/README.md
CHANGED
|
@@ -1,141 +1,84 @@
|
|
|
1
|
-
# Node.js MCP Code Change Tracker
|
|
2
|
-
|
|
3
|
-
A Node.js MCP server that records code changes and publishes them to Kafka.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
- Install via a single `npx` command
|
|
7
|
-
- Kafka producer powered by `kafkajs`
|
|
8
|
-
- Automatically records code changes to Kafka
|
|
9
|
-
- Failed queue persistence with replay support
|
|
10
|
-
|
|
11
|
-
## Installation
|
|
12
|
-
Example MCP config (default worker_id precedence: `./.ai_conifg/config.json` -> `MCP_TRACKER_WORKER_ID` -> OS username):
|
|
13
|
-
|
|
14
|
-
```json
|
|
15
|
-
{
|
|
16
|
-
"mcpServers": {
|
|
17
|
-
"code-change-tracker": {
|
|
18
|
-
"command": "npx",
|
|
19
|
-
"args": ["@yourorg/mcp-tracker"]
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Custom worker id:
|
|
26
|
-
|
|
27
|
-
```json
|
|
28
|
-
{
|
|
29
|
-
"mcpServers": {
|
|
30
|
-
"code-change-tracker": {
|
|
31
|
-
"command": "npx",
|
|
32
|
-
"args": ["@yourorg/mcp-tracker"],
|
|
33
|
-
"env": {
|
|
34
|
-
"MCP_TRACKER_WORKER_ID": "my-worker"
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Configuration
|
|
42
|
-
- `worker_id` resolution order:
|
|
43
|
-
1. `./.ai_conifg/config.json` 的 `worker_id`
|
|
44
|
-
2. 环境变量 `MCP_TRACKER_WORKER_ID`
|
|
45
|
-
3. 系统用户名(`os.userInfo().username`)
|
|
46
|
-
- `MCP_TRACKER_WORKER_ID`: 当本地 `.ai_conifg/config.json` 未提供有效 `worker_id` 时生效。
|
|
47
|
-
- Kafka config is currently hardcoded (see `src/config.ts`) and can be extended later.
|
|
48
|
-
|
|
49
|
-
## Tools
|
|
50
|
-
### `
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
- `
|
|
55
|
-
|
|
56
|
-
Output:
|
|
57
|
-
- `status`: `
|
|
58
|
-
- `
|
|
59
|
-
- `
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- `
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- `
|
|
84
|
-
- `
|
|
85
|
-
- `added_content`: original added content
|
|
86
|
-
|
|
87
|
-
### `cleanup_failed`
|
|
88
|
-
Input:
|
|
89
|
-
- `max_age_hours` (optional): default 168
|
|
90
|
-
|
|
91
|
-
Output:
|
|
92
|
-
- `status`
|
|
93
|
-
- `deleted_count`
|
|
94
|
-
|
|
95
|
-
### `replay_failed`
|
|
96
|
-
Input:
|
|
97
|
-
- `max_count` (optional): default 10
|
|
98
|
-
|
|
99
|
-
Output:
|
|
100
|
-
- `status`
|
|
101
|
-
- `replayed`
|
|
102
|
-
- `failed`
|
|
103
|
-
|
|
104
|
-
## Failed Queue
|
|
105
|
-
Failed events are stored in `~/.mcp-tracker/failed` as JSON files. Old files are cleaned by age and replayed in FIFO order.
|
|
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
|
-
|
|
138
|
-
## Development
|
|
139
|
-
- Build: `npm run build`
|
|
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`
|
|
1
|
+
# Node.js MCP Code Change Tracker
|
|
2
|
+
|
|
3
|
+
A Node.js MCP server that records code changes and publishes them to Kafka.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- Install via a single `npx` command
|
|
7
|
+
- Kafka producer powered by `kafkajs`
|
|
8
|
+
- Automatically records code changes to Kafka
|
|
9
|
+
- Failed queue persistence with replay support
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
Example MCP config (default worker_id precedence: `./.ai_conifg/config.json` -> `MCP_TRACKER_WORKER_ID` -> OS username):
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"code-change-tracker": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["@yourorg/mcp-tracker"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Custom worker id:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"code-change-tracker": {
|
|
31
|
+
"command": "npx",
|
|
32
|
+
"args": ["@yourorg/mcp-tracker"],
|
|
33
|
+
"env": {
|
|
34
|
+
"MCP_TRACKER_WORKER_ID": "my-worker"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Configuration
|
|
42
|
+
- `worker_id` resolution order:
|
|
43
|
+
1. `./.ai_conifg/config.json` 的 `worker_id`
|
|
44
|
+
2. 环境变量 `MCP_TRACKER_WORKER_ID`
|
|
45
|
+
3. 系统用户名(`os.userInfo().username`)
|
|
46
|
+
- `MCP_TRACKER_WORKER_ID`: 当本地 `.ai_conifg/config.json` 未提供有效 `worker_id` 时生效。
|
|
47
|
+
- Kafka config is currently hardcoded (see `src/config.ts`) and can be extended later.
|
|
48
|
+
|
|
49
|
+
## Tools
|
|
50
|
+
### `after_edit`
|
|
51
|
+
Input:
|
|
52
|
+
- `file_path`: file path
|
|
53
|
+
- `old_string`: previous content
|
|
54
|
+
- `new_string`: new content
|
|
55
|
+
|
|
56
|
+
Output:
|
|
57
|
+
- `status`: `recorded`
|
|
58
|
+
- `event_id`: event id
|
|
59
|
+
- `lines_changed`: total lines changed
|
|
60
|
+
- `added_content`: added lines content (may be truncated)
|
|
61
|
+
|
|
62
|
+
### `cleanup_failed`
|
|
63
|
+
Input:
|
|
64
|
+
- `max_age_hours` (optional): default 168
|
|
65
|
+
|
|
66
|
+
Output:
|
|
67
|
+
- `status`
|
|
68
|
+
- `deleted_count`
|
|
69
|
+
|
|
70
|
+
### `replay_failed`
|
|
71
|
+
Input:
|
|
72
|
+
- `max_count` (optional): default 10
|
|
73
|
+
|
|
74
|
+
Output:
|
|
75
|
+
- `status`
|
|
76
|
+
- `replayed`
|
|
77
|
+
- `failed`
|
|
78
|
+
|
|
79
|
+
## Failed Queue
|
|
80
|
+
Failed events are stored in `~/.mcp-tracker/failed` as JSON files. Old files are cleaned by age and replayed in FIFO order.
|
|
81
|
+
|
|
82
|
+
## Development
|
|
83
|
+
- Build: `npm run build`
|
|
84
|
+
- Test tools list: `echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/server.js`
|
package/dist/config.js
CHANGED
|
@@ -9,6 +9,7 @@ export const FAILED_QUEUE_CONFIG = {
|
|
|
9
9
|
};
|
|
10
10
|
function resolveWorkerId() {
|
|
11
11
|
const localConfigPath = path.resolve(process.cwd(), '.ai_config', 'config.json');
|
|
12
|
+
console.log('[mcp-tracker] localConfigPath:', localConfigPath);
|
|
12
13
|
try {
|
|
13
14
|
if (fs.existsSync(localConfigPath)) {
|
|
14
15
|
const raw = fs.readFileSync(localConfigPath, 'utf-8');
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
import { execSync } from 'node:child_process';
|
|
5
4
|
import { randomUUID } from 'node:crypto';
|
|
6
5
|
import { diffLines } from 'diff';
|
|
7
6
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -41,27 +40,12 @@ function getRepoNameFromGitConfig(cwd) {
|
|
|
41
40
|
return null;
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
|
-
function getGitRepoRoot(cwd) {
|
|
45
|
-
try {
|
|
46
|
-
const output = execSync('git rev-parse --show-toplevel', {
|
|
47
|
-
cwd,
|
|
48
|
-
encoding: 'utf8',
|
|
49
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
50
|
-
});
|
|
51
|
-
const repoRoot = output.trim();
|
|
52
|
-
return repoRoot || null;
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
43
|
async function getGitInfo(cwd) {
|
|
59
|
-
const
|
|
60
|
-
const repoNameFromGitConfig = getRepoNameFromGitConfig(repoRoot);
|
|
44
|
+
const repoNameFromGitConfig = getRepoNameFromGitConfig(cwd);
|
|
61
45
|
if (repoNameFromGitConfig) {
|
|
62
|
-
return { repo_name: repoNameFromGitConfig
|
|
46
|
+
return { repo_name: repoNameFromGitConfig };
|
|
63
47
|
}
|
|
64
|
-
const configPath = path.join(
|
|
48
|
+
const configPath = path.join(cwd, '.gitlab', 'config.json');
|
|
65
49
|
if (!fs.existsSync(configPath)) {
|
|
66
50
|
return null;
|
|
67
51
|
}
|
|
@@ -72,25 +56,12 @@ async function getGitInfo(cwd) {
|
|
|
72
56
|
if (!repoName) {
|
|
73
57
|
return null;
|
|
74
58
|
}
|
|
75
|
-
return { repo_name: repoName
|
|
59
|
+
return { repo_name: repoName };
|
|
76
60
|
}
|
|
77
61
|
catch {
|
|
78
62
|
return null;
|
|
79
63
|
}
|
|
80
64
|
}
|
|
81
|
-
function toRepoRelativePath(filePath, repoRoot) {
|
|
82
|
-
if (!repoRoot) {
|
|
83
|
-
return filePath;
|
|
84
|
-
}
|
|
85
|
-
const relativePath = path.relative(repoRoot, filePath);
|
|
86
|
-
if (!relativePath || relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
87
|
-
return filePath;
|
|
88
|
-
}
|
|
89
|
-
return relativePath.replace(/\\/g, '/');
|
|
90
|
-
}
|
|
91
|
-
function getKafkaFilePath(filePath, gitInfo) {
|
|
92
|
-
return toRepoRelativePath(filePath, gitInfo?.repo_root);
|
|
93
|
-
}
|
|
94
65
|
function getAddedLines(oldStr, newStr) {
|
|
95
66
|
const parts = diffLines(oldStr, newStr);
|
|
96
67
|
let lineCount = 0;
|
|
@@ -126,93 +97,9 @@ function countDiff(oldStr, newStr) {
|
|
|
126
97
|
}
|
|
127
98
|
return { added, removed };
|
|
128
99
|
}
|
|
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
|
-
}
|
|
147
100
|
function getFailedDir() {
|
|
148
101
|
return path.join(os.homedir(), '.mcp-tracker', 'failed');
|
|
149
102
|
}
|
|
150
|
-
function getSnapshotsDir() {
|
|
151
|
-
return path.join(os.homedir(), '.mcp-tracker', 'snapshots');
|
|
152
|
-
}
|
|
153
|
-
function ensureSnapshotsDirExists() {
|
|
154
|
-
const dir = getSnapshotsDir();
|
|
155
|
-
if (!fs.existsSync(dir)) {
|
|
156
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
157
|
-
}
|
|
158
|
-
return dir;
|
|
159
|
-
}
|
|
160
|
-
function getSnapshotPath(snapshotId) {
|
|
161
|
-
return path.join(getSnapshotsDir(), `${snapshotId}.json`);
|
|
162
|
-
}
|
|
163
|
-
function cleanupSnapshotsOnStartup() {
|
|
164
|
-
const dir = ensureSnapshotsDirExists();
|
|
165
|
-
const entries = fs.readdirSync(dir);
|
|
166
|
-
let deleted = 0;
|
|
167
|
-
for (const name of entries) {
|
|
168
|
-
const filePath = path.join(dir, name);
|
|
169
|
-
const stat = fs.statSync(filePath);
|
|
170
|
-
if (!stat.isFile()) {
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
fs.unlinkSync(filePath);
|
|
174
|
-
deleted += 1;
|
|
175
|
-
}
|
|
176
|
-
return { status: 'cleaned', deleted_count: deleted };
|
|
177
|
-
}
|
|
178
|
-
function saveSnapshot(snapshot) {
|
|
179
|
-
const dir = ensureSnapshotsDirExists();
|
|
180
|
-
const filePath = path.join(dir, `${snapshot.snapshot_id}.json`);
|
|
181
|
-
fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2), 'utf8');
|
|
182
|
-
return filePath;
|
|
183
|
-
}
|
|
184
|
-
function loadSnapshot(snapshotId) {
|
|
185
|
-
const filePath = getSnapshotPath(snapshotId);
|
|
186
|
-
if (!fs.existsSync(filePath)) {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
const raw = fs.readFileSync(filePath, 'utf8');
|
|
190
|
-
return JSON.parse(raw);
|
|
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
|
-
}
|
|
210
|
-
function deleteSnapshot(snapshotId) {
|
|
211
|
-
const filePath = getSnapshotPath(snapshotId);
|
|
212
|
-
if (fs.existsSync(filePath)) {
|
|
213
|
-
fs.unlinkSync(filePath);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
103
|
function cleanupFailed(maxAgeHours = 168) {
|
|
217
104
|
const dir = getFailedDir();
|
|
218
105
|
if (!fs.existsSync(dir)) {
|
|
@@ -280,89 +167,28 @@ async function replayFailed(maxCount, config) {
|
|
|
280
167
|
}
|
|
281
168
|
return { status: 'completed', replayed, failed };
|
|
282
169
|
}
|
|
283
|
-
function validateFilePath(filePath) {
|
|
284
|
-
const normalizedPath = filePath.trim();
|
|
285
|
-
if (!normalizedPath) {
|
|
286
|
-
throw new Error('file_path is required');
|
|
287
|
-
}
|
|
288
|
-
return normalizedPath;
|
|
289
|
-
}
|
|
290
|
-
function readFileContent(filePath) {
|
|
291
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
292
|
-
}
|
|
293
|
-
function getFailedEventPath(eventId) {
|
|
294
|
-
return path.join(getFailedDir(), `${eventId}.json`);
|
|
295
|
-
}
|
|
296
|
-
function handleBeforeEdit(input) {
|
|
297
|
-
const filePath = validateFilePath(input.file_path);
|
|
298
|
-
const content = fs.existsSync(filePath) ? readFileContent(filePath) : '';
|
|
299
|
-
const snapshotId = randomUUID();
|
|
300
|
-
saveSnapshot({
|
|
301
|
-
snapshot_id: snapshotId,
|
|
302
|
-
file_path: filePath,
|
|
303
|
-
created_at: new Date().toISOString(),
|
|
304
|
-
content
|
|
305
|
-
});
|
|
306
|
-
return JSON.stringify({
|
|
307
|
-
status: 'snapshotted',
|
|
308
|
-
snapshot_id: snapshotId,
|
|
309
|
-
file_path: filePath
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
170
|
async function handleAfterEdit(input, config) {
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const diffStats = countDiff(oldStr, newStr);
|
|
329
|
-
const event = await buildChangeEvent({
|
|
330
|
-
filePath,
|
|
331
|
-
operation: 'edit',
|
|
332
|
-
addedContent: addedLines,
|
|
333
|
-
added: diffStats.added,
|
|
334
|
-
removed: diffStats.removed,
|
|
335
|
-
config
|
|
336
|
-
});
|
|
337
|
-
await publishToKafka(config, event);
|
|
338
|
-
if (fs.existsSync(getFailedEventPath(event.event_id))) {
|
|
339
|
-
return JSON.stringify({ success: false });
|
|
340
|
-
}
|
|
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
|
-
});
|
|
171
|
+
const addedLines = getAddedLines(input.old_string, input.new_string);
|
|
172
|
+
const diffStats = countDiff(input.old_string, input.new_string);
|
|
173
|
+
const gitInfo = await getGitInfo(process.cwd());
|
|
174
|
+
const event = {
|
|
175
|
+
event_id: randomUUID(),
|
|
176
|
+
timestamp: new Date().toISOString(),
|
|
177
|
+
worker_id: config.worker_id,
|
|
178
|
+
git: gitInfo,
|
|
179
|
+
file: { path: input.file_path, operation: 'edit' },
|
|
180
|
+
changes: {
|
|
181
|
+
added_content: addedLines,
|
|
182
|
+
added: diffStats.added,
|
|
183
|
+
removed: diffStats.removed
|
|
184
|
+
}
|
|
185
|
+
};
|
|
359
186
|
await publishToKafka(config, event);
|
|
360
|
-
const status = fs.existsSync(getFailedEventPath(event.event_id)) ? 'queued_for_retry' : 'recorded';
|
|
361
187
|
return JSON.stringify({
|
|
362
|
-
status,
|
|
188
|
+
status: 'recorded',
|
|
363
189
|
event_id: event.event_id,
|
|
364
|
-
lines_changed: added,
|
|
365
|
-
added_content:
|
|
190
|
+
lines_changed: diffStats.added + diffStats.removed,
|
|
191
|
+
added_content: addedLines
|
|
366
192
|
});
|
|
367
193
|
}
|
|
368
194
|
function handleCleanupFailed(input) {
|
|
@@ -375,39 +201,17 @@ async function handleReplayFailed(input, config) {
|
|
|
375
201
|
const result = await replayFailed(maxCount, config);
|
|
376
202
|
return JSON.stringify({ status: result.status, replayed: result.replayed, failed: result.failed });
|
|
377
203
|
}
|
|
378
|
-
const BEFORE_EDIT_TOOL_SCHEMA = {
|
|
379
|
-
name: 'before_edit',
|
|
380
|
-
description: 'Save a server-side snapshot before file edits.',
|
|
381
|
-
inputSchema: {
|
|
382
|
-
type: 'object',
|
|
383
|
-
properties: {
|
|
384
|
-
file_path: { type: 'string' }
|
|
385
|
-
},
|
|
386
|
-
required: ['file_path']
|
|
387
|
-
}
|
|
388
|
-
};
|
|
389
204
|
const AFTER_EDIT_TOOL_SCHEMA = {
|
|
390
205
|
name: 'after_edit',
|
|
391
|
-
description: 'Record code changes after file edits
|
|
206
|
+
description: 'Record code changes after file edits.',
|
|
392
207
|
inputSchema: {
|
|
393
208
|
type: 'object',
|
|
394
209
|
properties: {
|
|
395
210
|
file_path: { type: 'string' },
|
|
396
|
-
|
|
211
|
+
old_string: { type: 'string' },
|
|
212
|
+
new_string: { type: 'string' }
|
|
397
213
|
},
|
|
398
|
-
required: ['file_path', '
|
|
399
|
-
}
|
|
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']
|
|
214
|
+
required: ['file_path', 'old_string', 'new_string']
|
|
411
215
|
}
|
|
412
216
|
};
|
|
413
217
|
const CLEANUP_FAILED_TOOL_SCHEMA = {
|
|
@@ -432,7 +236,6 @@ const REPLAY_FAILED_TOOL_SCHEMA = {
|
|
|
432
236
|
};
|
|
433
237
|
async function main() {
|
|
434
238
|
const config = loadConfig();
|
|
435
|
-
cleanupSnapshotsOnStartup();
|
|
436
239
|
await initProducer(config);
|
|
437
240
|
const server = new Server({
|
|
438
241
|
name: 'code-change-tracker',
|
|
@@ -443,23 +246,15 @@ async function main() {
|
|
|
443
246
|
}
|
|
444
247
|
});
|
|
445
248
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
446
|
-
tools: [
|
|
249
|
+
tools: [AFTER_EDIT_TOOL_SCHEMA, CLEANUP_FAILED_TOOL_SCHEMA, REPLAY_FAILED_TOOL_SCHEMA]
|
|
447
250
|
}));
|
|
448
251
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
449
252
|
const name = request.params.name;
|
|
450
253
|
const input = request.params.arguments ?? {};
|
|
451
|
-
if (name === 'before_edit') {
|
|
452
|
-
const result = handleBeforeEdit(input);
|
|
453
|
-
return { content: [{ type: 'text', text: result }] };
|
|
454
|
-
}
|
|
455
254
|
if (name === 'after_edit') {
|
|
456
255
|
const result = await handleAfterEdit(input, config);
|
|
457
256
|
return { content: [{ type: 'text', text: result }] };
|
|
458
257
|
}
|
|
459
|
-
if (name === 'direct_add') {
|
|
460
|
-
const result = await handleDirectAdd(input, config);
|
|
461
|
-
return { content: [{ type: 'text', text: result }] };
|
|
462
|
-
}
|
|
463
258
|
if (name === 'cleanup_failed') {
|
|
464
259
|
const result = handleCleanupFailed(input);
|
|
465
260
|
return { content: [{ type: 'text', text: result }] };
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "ronds-mcp-tracker",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"bin": {
|
|
6
|
-
"ronds-mcp-tracker": "./bin/cli.js"
|
|
7
|
-
},
|
|
8
|
-
"scripts": {
|
|
9
|
-
"build": "tsc",
|
|
10
|
-
"prepublishOnly": "npm run build",
|
|
11
|
-
"dev": "tsc && node dist/server.js",
|
|
12
|
-
"test": "mcp-inspector node dist/server.js"
|
|
13
|
-
},
|
|
14
|
-
"dependencies": {
|
|
15
|
-
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
16
|
-
"ajv": "^8.17.1",
|
|
17
|
-
"ajv-formats": "^3.0.1",
|
|
18
|
-
"diff": "^7.0.0",
|
|
19
|
-
"kafkajs": "^2.2.4"
|
|
20
|
-
},
|
|
21
|
-
"devDependencies": {
|
|
22
|
-
"@types/diff": "^7.0.0",
|
|
23
|
-
"@types/node": "^20.0.0",
|
|
24
|
-
"typescript": "^5.0.0"
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "ronds-mcp-tracker",
|
|
3
|
+
"version": "0.1.10",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ronds-mcp-tracker": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepublishOnly": "npm run build",
|
|
11
|
+
"dev": "tsc && node dist/server.js",
|
|
12
|
+
"test": "mcp-inspector node dist/server.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
16
|
+
"ajv": "^8.17.1",
|
|
17
|
+
"ajv-formats": "^3.0.1",
|
|
18
|
+
"diff": "^7.0.0",
|
|
19
|
+
"kafkajs": "^2.2.4"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/diff": "^7.0.0",
|
|
23
|
+
"@types/node": "^20.0.0",
|
|
24
|
+
"typescript": "^5.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|