ronds-mcp-tracker 0.1.6 → 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.
- package/README.md +61 -4
- package/dist/config.js +0 -1
- package/dist/server.js +231 -24
- 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
|
-
- `
|
|
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
|
|
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/config.js
CHANGED
|
@@ -9,7 +9,6 @@ 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);
|
|
13
12
|
try {
|
|
14
13
|
if (fs.existsSync(localConfigPath)) {
|
|
15
14
|
const raw = fs.readFileSync(localConfigPath, 'utf-8');
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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';
|
|
4
5
|
import { randomUUID } from 'node:crypto';
|
|
5
6
|
import { diffLines } from 'diff';
|
|
6
7
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
@@ -40,12 +41,27 @@ function getRepoNameFromGitConfig(cwd) {
|
|
|
40
41
|
return null;
|
|
41
42
|
}
|
|
42
43
|
}
|
|
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
|
+
}
|
|
43
58
|
async function getGitInfo(cwd) {
|
|
44
|
-
const
|
|
59
|
+
const repoRoot = getGitRepoRoot(cwd) ?? cwd;
|
|
60
|
+
const repoNameFromGitConfig = getRepoNameFromGitConfig(repoRoot);
|
|
45
61
|
if (repoNameFromGitConfig) {
|
|
46
|
-
return { repo_name: repoNameFromGitConfig };
|
|
62
|
+
return { repo_name: repoNameFromGitConfig, repo_root: repoRoot };
|
|
47
63
|
}
|
|
48
|
-
const configPath = path.join(
|
|
64
|
+
const configPath = path.join(repoRoot, '.gitlab', 'config.json');
|
|
49
65
|
if (!fs.existsSync(configPath)) {
|
|
50
66
|
return null;
|
|
51
67
|
}
|
|
@@ -56,12 +72,25 @@ async function getGitInfo(cwd) {
|
|
|
56
72
|
if (!repoName) {
|
|
57
73
|
return null;
|
|
58
74
|
}
|
|
59
|
-
return { repo_name: repoName };
|
|
75
|
+
return { repo_name: repoName, repo_root: repoRoot };
|
|
60
76
|
}
|
|
61
77
|
catch {
|
|
62
78
|
return null;
|
|
63
79
|
}
|
|
64
80
|
}
|
|
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
|
+
}
|
|
65
94
|
function getAddedLines(oldStr, newStr) {
|
|
66
95
|
const parts = diffLines(oldStr, newStr);
|
|
67
96
|
let lineCount = 0;
|
|
@@ -97,9 +126,75 @@ function countDiff(oldStr, newStr) {
|
|
|
97
126
|
}
|
|
98
127
|
return { added, removed };
|
|
99
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
|
+
}
|
|
100
147
|
function getFailedDir() {
|
|
101
148
|
return path.join(os.homedir(), '.mcp-tracker', 'failed');
|
|
102
149
|
}
|
|
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 deleteSnapshot(snapshotId) {
|
|
193
|
+
const filePath = getSnapshotPath(snapshotId);
|
|
194
|
+
if (fs.existsSync(filePath)) {
|
|
195
|
+
fs.unlinkSync(filePath);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
103
198
|
function cleanupFailed(maxAgeHours = 168) {
|
|
104
199
|
const dir = getFailedDir();
|
|
105
200
|
if (!fs.existsSync(dir)) {
|
|
@@ -167,23 +262,80 @@ async function replayFailed(maxCount, config) {
|
|
|
167
262
|
}
|
|
168
263
|
return { status: 'completed', replayed, failed };
|
|
169
264
|
}
|
|
265
|
+
function validateFilePath(filePath) {
|
|
266
|
+
const normalizedPath = filePath.trim();
|
|
267
|
+
if (!normalizedPath) {
|
|
268
|
+
throw new Error('file_path is required');
|
|
269
|
+
}
|
|
270
|
+
return normalizedPath;
|
|
271
|
+
}
|
|
272
|
+
function readFileContent(filePath) {
|
|
273
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
274
|
+
}
|
|
275
|
+
function getFailedEventPath(eventId) {
|
|
276
|
+
return path.join(getFailedDir(), `${eventId}.json`);
|
|
277
|
+
}
|
|
278
|
+
function handleBeforeEdit(input) {
|
|
279
|
+
const filePath = validateFilePath(input.file_path);
|
|
280
|
+
const content = fs.existsSync(filePath) ? readFileContent(filePath) : '';
|
|
281
|
+
const snapshotId = randomUUID();
|
|
282
|
+
saveSnapshot({
|
|
283
|
+
snapshot_id: snapshotId,
|
|
284
|
+
file_path: filePath,
|
|
285
|
+
created_at: new Date().toISOString(),
|
|
286
|
+
content
|
|
287
|
+
});
|
|
288
|
+
return JSON.stringify({
|
|
289
|
+
status: 'snapshotted',
|
|
290
|
+
snapshot_id: snapshotId,
|
|
291
|
+
file_path: filePath
|
|
292
|
+
});
|
|
293
|
+
}
|
|
170
294
|
async function handleAfterEdit(input, config) {
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
295
|
+
const filePath = validateFilePath(input.file_path);
|
|
296
|
+
const snapshotId = input.snapshot_id?.trim();
|
|
297
|
+
if (!snapshotId) {
|
|
298
|
+
throw new Error('snapshot_id is required');
|
|
299
|
+
}
|
|
300
|
+
const snapshot = loadSnapshot(snapshotId);
|
|
301
|
+
if (!snapshot) {
|
|
302
|
+
return JSON.stringify({
|
|
303
|
+
status: 'snapshot_not_found',
|
|
304
|
+
snapshot_id: snapshotId,
|
|
305
|
+
file_path: filePath
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (snapshot.file_path !== filePath) {
|
|
309
|
+
return JSON.stringify({
|
|
310
|
+
status: 'snapshot_file_path_mismatch',
|
|
311
|
+
snapshot_id: snapshotId,
|
|
312
|
+
file_path: filePath,
|
|
313
|
+
snapshot_file_path: snapshot.file_path
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const oldStr = snapshot.content;
|
|
317
|
+
const newStr = readFileContent(filePath);
|
|
318
|
+
const addedLines = getAddedLines(oldStr, newStr);
|
|
319
|
+
const diffStats = countDiff(oldStr, newStr);
|
|
320
|
+
const event = await buildChangeEvent({
|
|
321
|
+
filePath,
|
|
322
|
+
operation: 'edit',
|
|
323
|
+
addedContent: addedLines,
|
|
324
|
+
added: diffStats.added,
|
|
325
|
+
removed: diffStats.removed,
|
|
326
|
+
config
|
|
327
|
+
});
|
|
186
328
|
await publishToKafka(config, event);
|
|
329
|
+
if (fs.existsSync(getFailedEventPath(event.event_id))) {
|
|
330
|
+
return JSON.stringify({
|
|
331
|
+
status: 'queued_for_retry',
|
|
332
|
+
event_id: event.event_id,
|
|
333
|
+
snapshot_id: snapshotId,
|
|
334
|
+
lines_changed: diffStats.added + diffStats.removed,
|
|
335
|
+
added_content: addedLines
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
deleteSnapshot(snapshotId);
|
|
187
339
|
return JSON.stringify({
|
|
188
340
|
status: 'recorded',
|
|
189
341
|
event_id: event.event_id,
|
|
@@ -191,6 +343,30 @@ async function handleAfterEdit(input, config) {
|
|
|
191
343
|
added_content: addedLines
|
|
192
344
|
});
|
|
193
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
|
+
}
|
|
194
370
|
function handleCleanupFailed(input) {
|
|
195
371
|
const maxAgeHours = input.max_age_hours ?? 168;
|
|
196
372
|
const result = cleanupFailed(maxAgeHours);
|
|
@@ -201,17 +377,39 @@ async function handleReplayFailed(input, config) {
|
|
|
201
377
|
const result = await replayFailed(maxCount, config);
|
|
202
378
|
return JSON.stringify({ status: result.status, replayed: result.replayed, failed: result.failed });
|
|
203
379
|
}
|
|
380
|
+
const BEFORE_EDIT_TOOL_SCHEMA = {
|
|
381
|
+
name: 'before_edit',
|
|
382
|
+
description: 'Save a server-side snapshot before file edits.',
|
|
383
|
+
inputSchema: {
|
|
384
|
+
type: 'object',
|
|
385
|
+
properties: {
|
|
386
|
+
file_path: { type: 'string' }
|
|
387
|
+
},
|
|
388
|
+
required: ['file_path']
|
|
389
|
+
}
|
|
390
|
+
};
|
|
204
391
|
const AFTER_EDIT_TOOL_SCHEMA = {
|
|
205
392
|
name: 'after_edit',
|
|
206
|
-
description: 'Record code changes after file edits.',
|
|
393
|
+
description: 'Record code changes after file edits using a saved snapshot.',
|
|
394
|
+
inputSchema: {
|
|
395
|
+
type: 'object',
|
|
396
|
+
properties: {
|
|
397
|
+
file_path: { type: 'string' },
|
|
398
|
+
snapshot_id: { type: 'string' }
|
|
399
|
+
},
|
|
400
|
+
required: ['file_path', 'snapshot_id']
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
const DIRECT_ADD_TOOL_SCHEMA = {
|
|
404
|
+
name: 'direct_add',
|
|
405
|
+
description: 'Record added content directly when no pre-edit snapshot is available.',
|
|
207
406
|
inputSchema: {
|
|
208
407
|
type: 'object',
|
|
209
408
|
properties: {
|
|
210
409
|
file_path: { type: 'string' },
|
|
211
|
-
|
|
212
|
-
new_string: { type: 'string' }
|
|
410
|
+
added_content: { type: 'string' }
|
|
213
411
|
},
|
|
214
|
-
required: ['file_path', '
|
|
412
|
+
required: ['file_path', 'added_content']
|
|
215
413
|
}
|
|
216
414
|
};
|
|
217
415
|
const CLEANUP_FAILED_TOOL_SCHEMA = {
|
|
@@ -236,6 +434,7 @@ const REPLAY_FAILED_TOOL_SCHEMA = {
|
|
|
236
434
|
};
|
|
237
435
|
async function main() {
|
|
238
436
|
const config = loadConfig();
|
|
437
|
+
cleanupSnapshotsOnStartup();
|
|
239
438
|
await initProducer(config);
|
|
240
439
|
const server = new Server({
|
|
241
440
|
name: 'code-change-tracker',
|
|
@@ -246,15 +445,23 @@ async function main() {
|
|
|
246
445
|
}
|
|
247
446
|
});
|
|
248
447
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
249
|
-
tools: [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]
|
|
250
449
|
}));
|
|
251
450
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
252
451
|
const name = request.params.name;
|
|
253
452
|
const input = request.params.arguments ?? {};
|
|
453
|
+
if (name === 'before_edit') {
|
|
454
|
+
const result = handleBeforeEdit(input);
|
|
455
|
+
return { content: [{ type: 'text', text: result }] };
|
|
456
|
+
}
|
|
254
457
|
if (name === 'after_edit') {
|
|
255
458
|
const result = await handleAfterEdit(input, config);
|
|
256
459
|
return { content: [{ type: 'text', text: result }] };
|
|
257
460
|
}
|
|
461
|
+
if (name === 'direct_add') {
|
|
462
|
+
const result = await handleDirectAdd(input, config);
|
|
463
|
+
return { content: [{ type: 'text', text: result }] };
|
|
464
|
+
}
|
|
258
465
|
if (name === 'cleanup_failed') {
|
|
259
466
|
const result = handleCleanupFailed(input);
|
|
260
467
|
return { content: [{ type: 'text', text: result }] };
|