cursor-guard 2.1.1 → 3.1.0
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 +63 -11
- package/README.zh-CN.md +345 -293
- package/ROADMAP.md +834 -0
- package/SKILL.md +617 -557
- package/package.json +14 -5
- package/references/config-reference.md +175 -175
- package/references/config-reference.zh-CN.md +175 -175
- package/references/cursor-guard.example.json +0 -6
- package/references/lib/auto-backup.js +257 -530
- package/references/lib/core/backups.js +357 -0
- package/references/lib/core/core.test.js +859 -0
- package/references/lib/core/doctor-fix.js +237 -0
- package/references/lib/core/doctor.js +248 -0
- package/references/lib/core/restore.js +305 -0
- package/references/lib/core/snapshot.js +173 -0
- package/references/lib/core/status.js +163 -0
- package/references/lib/guard-doctor.js +46 -238
- package/references/lib/utils.js +371 -371
- package/references/mcp/mcp.test.js +279 -0
- package/references/mcp/server.js +198 -0
- package/references/quickstart.zh-CN.md +342 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
let passed = 0;
|
|
10
|
+
let failed = 0;
|
|
11
|
+
let serverProcess = null;
|
|
12
|
+
let msgId = 0;
|
|
13
|
+
|
|
14
|
+
function log(color, sym, msg) {
|
|
15
|
+
const c = { green: 32, red: 31 }[color] || 0;
|
|
16
|
+
console.log(` \x1b[${c}m${sym}\x1b[0m ${msg}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createTempGitRepo() {
|
|
20
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'guard-mcp-test-'));
|
|
21
|
+
execFileSync('git', ['init'], { cwd: tmpDir, stdio: 'pipe' });
|
|
22
|
+
execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: tmpDir, stdio: 'pipe' });
|
|
23
|
+
execFileSync('git', ['config', 'user.name', 'Test'], { cwd: tmpDir, stdio: 'pipe' });
|
|
24
|
+
fs.writeFileSync(path.join(tmpDir, 'hello.txt'), 'hello world');
|
|
25
|
+
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
26
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'app.js'), 'console.log("app");');
|
|
27
|
+
execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
|
|
28
|
+
execFileSync('git', ['commit', '-m', 'initial', '--no-verify'], { cwd: tmpDir, stdio: 'pipe' });
|
|
29
|
+
return tmpDir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function startServer() {
|
|
33
|
+
const serverPath = path.join(__dirname, 'server.js');
|
|
34
|
+
serverProcess = spawn('node', [serverPath], {
|
|
35
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
36
|
+
});
|
|
37
|
+
serverProcess.stderr.on('data', () => {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sendMessage(msg) {
|
|
41
|
+
serverProcess.stdin.write(JSON.stringify(msg) + '\n');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readResponse() {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
let buffer = '';
|
|
47
|
+
const timeout = setTimeout(() => {
|
|
48
|
+
serverProcess.stdout.removeListener('data', onData);
|
|
49
|
+
reject(new Error('timeout waiting for response'));
|
|
50
|
+
}, 15000);
|
|
51
|
+
|
|
52
|
+
function onData(chunk) {
|
|
53
|
+
buffer += chunk.toString();
|
|
54
|
+
const lines = buffer.split('\n');
|
|
55
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
56
|
+
const line = lines[i].trim();
|
|
57
|
+
if (!line) continue;
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(line);
|
|
60
|
+
if (parsed.id !== undefined || parsed.result !== undefined) {
|
|
61
|
+
clearTimeout(timeout);
|
|
62
|
+
serverProcess.stdout.removeListener('data', onData);
|
|
63
|
+
resolve(parsed);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
} catch { /* not valid JSON yet, continue */ }
|
|
67
|
+
}
|
|
68
|
+
buffer = lines[lines.length - 1];
|
|
69
|
+
}
|
|
70
|
+
serverProcess.stdout.on('data', onData);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function rpc(method, params) {
|
|
75
|
+
const id = ++msgId;
|
|
76
|
+
sendMessage({ jsonrpc: '2.0', id, method, params });
|
|
77
|
+
return await readResponse();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function test(name, fn) {
|
|
81
|
+
try {
|
|
82
|
+
await fn();
|
|
83
|
+
passed++;
|
|
84
|
+
log('green', '✓', name);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
failed++;
|
|
87
|
+
log('red', '✗', name);
|
|
88
|
+
console.log(` ${e.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function run() {
|
|
93
|
+
const tmpDir = createTempGitRepo();
|
|
94
|
+
|
|
95
|
+
console.log('\nMCP Server:');
|
|
96
|
+
|
|
97
|
+
startServer();
|
|
98
|
+
|
|
99
|
+
// Initialize
|
|
100
|
+
const initResp = await rpc('initialize', {
|
|
101
|
+
protocolVersion: '2024-11-05',
|
|
102
|
+
capabilities: {},
|
|
103
|
+
clientInfo: { name: 'test-client', version: '1.0' },
|
|
104
|
+
});
|
|
105
|
+
sendMessage({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
106
|
+
|
|
107
|
+
await test('initialize returns server info', () => {
|
|
108
|
+
if (!initResp.result) throw new Error('no result');
|
|
109
|
+
if (!initResp.result.serverInfo) throw new Error('no serverInfo');
|
|
110
|
+
if (initResp.result.serverInfo.name !== 'cursor-guard') throw new Error(`wrong name: ${initResp.result.serverInfo.name}`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// List tools
|
|
114
|
+
const toolsResp = await rpc('tools/list', {});
|
|
115
|
+
|
|
116
|
+
await test('lists 7 tools', () => {
|
|
117
|
+
const tools = toolsResp.result.tools;
|
|
118
|
+
if (!tools) throw new Error('no tools');
|
|
119
|
+
if (tools.length !== 7) throw new Error(`expected 7 tools, got ${tools.length}`);
|
|
120
|
+
const names = tools.map(t => t.name).sort();
|
|
121
|
+
const expected = ['backup_status', 'doctor', 'doctor_fix', 'list_backups', 'restore_file', 'restore_project', 'snapshot_now'].sort();
|
|
122
|
+
if (JSON.stringify(names) !== JSON.stringify(expected)) {
|
|
123
|
+
throw new Error(`tool names mismatch: ${JSON.stringify(names)}`);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Call doctor
|
|
128
|
+
const doctorResp = await rpc('tools/call', {
|
|
129
|
+
name: 'doctor',
|
|
130
|
+
arguments: { path: tmpDir },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await test('doctor returns structured checks', () => {
|
|
134
|
+
const content = doctorResp.result.content[0].text;
|
|
135
|
+
const data = JSON.parse(content);
|
|
136
|
+
if (!data.checks) throw new Error('no checks');
|
|
137
|
+
if (!data.summary) throw new Error('no summary');
|
|
138
|
+
if (typeof data.summary.pass !== 'number') throw new Error('summary.pass not a number');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Call snapshot_now
|
|
142
|
+
const snapResp = await rpc('tools/call', {
|
|
143
|
+
name: 'snapshot_now',
|
|
144
|
+
arguments: { path: tmpDir, strategy: 'git' },
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await test('snapshot_now creates git snapshot', () => {
|
|
148
|
+
const content = snapResp.result.content[0].text;
|
|
149
|
+
const data = JSON.parse(content);
|
|
150
|
+
if (!data.git) throw new Error('no git result');
|
|
151
|
+
if (data.git.status !== 'created' && data.git.status !== 'skipped') {
|
|
152
|
+
throw new Error(`unexpected status: ${data.git.status}`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Call list_backups
|
|
157
|
+
const listResp = await rpc('tools/call', {
|
|
158
|
+
name: 'list_backups',
|
|
159
|
+
arguments: { path: tmpDir },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await test('list_backups returns sources', () => {
|
|
163
|
+
const content = listResp.result.content[0].text;
|
|
164
|
+
const data = JSON.parse(content);
|
|
165
|
+
if (!Array.isArray(data.sources)) throw new Error('sources not an array');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Setup for restore tests
|
|
169
|
+
const headHash = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: tmpDir, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
170
|
+
fs.writeFileSync(path.join(tmpDir, 'hello.txt'), 'changed');
|
|
171
|
+
execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
|
|
172
|
+
execFileSync('git', ['commit', '-m', 'change', '--no-verify'], { cwd: tmpDir, stdio: 'pipe' });
|
|
173
|
+
|
|
174
|
+
// Call restore_project (preview)
|
|
175
|
+
const previewResp = await rpc('tools/call', {
|
|
176
|
+
name: 'restore_project',
|
|
177
|
+
arguments: { path: tmpDir, source: headHash, preview: true },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await test('restore_project returns preview', () => {
|
|
181
|
+
const content = previewResp.result.content[0].text;
|
|
182
|
+
const data = JSON.parse(content);
|
|
183
|
+
if (data.status !== 'ok') throw new Error(`status: ${data.status}`);
|
|
184
|
+
if (!Array.isArray(data.files)) throw new Error('files not an array');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Call restore_project (execute mode)
|
|
188
|
+
// Re-stage the change so we have something to restore
|
|
189
|
+
fs.writeFileSync(path.join(tmpDir, 'hello.txt'), 'changed-again');
|
|
190
|
+
execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
|
|
191
|
+
execFileSync('git', ['commit', '-m', 'change again', '--no-verify'], { cwd: tmpDir, stdio: 'pipe' });
|
|
192
|
+
|
|
193
|
+
const execResp = await rpc('tools/call', {
|
|
194
|
+
name: 'restore_project',
|
|
195
|
+
arguments: { path: tmpDir, source: headHash, preview: false, preserve_current: true },
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await test('restore_project executes restore', () => {
|
|
199
|
+
const content = execResp.result.content[0].text;
|
|
200
|
+
const data = JSON.parse(content);
|
|
201
|
+
if (data.status !== 'restored') throw new Error(`status: ${data.status}, error: ${data.error}`);
|
|
202
|
+
if (typeof data.filesRestored !== 'number') throw new Error('filesRestored missing');
|
|
203
|
+
const actual = fs.readFileSync(path.join(tmpDir, 'hello.txt'), 'utf-8');
|
|
204
|
+
if (actual !== 'hello world') throw new Error(`content mismatch: "${actual}"`);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Call restore_file
|
|
208
|
+
fs.writeFileSync(path.join(tmpDir, 'hello.txt'), 'overwritten');
|
|
209
|
+
execFileSync('git', ['add', '-A'], { cwd: tmpDir, stdio: 'pipe' });
|
|
210
|
+
execFileSync('git', ['commit', '-m', 'overwrite', '--no-verify'], { cwd: tmpDir, stdio: 'pipe' });
|
|
211
|
+
|
|
212
|
+
const restoreResp = await rpc('tools/call', {
|
|
213
|
+
name: 'restore_file',
|
|
214
|
+
arguments: { path: tmpDir, file: 'hello.txt', source: headHash, preserve_current: false },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
await test('restore_file restores successfully', () => {
|
|
218
|
+
const content = restoreResp.result.content[0].text;
|
|
219
|
+
const data = JSON.parse(content);
|
|
220
|
+
if (data.status !== 'restored') throw new Error(`status: ${data.status}, error: ${data.error}`);
|
|
221
|
+
const actual = fs.readFileSync(path.join(tmpDir, 'hello.txt'), 'utf-8');
|
|
222
|
+
if (actual !== 'hello world') throw new Error(`content mismatch: "${actual}"`);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Call backup_status
|
|
226
|
+
const statusResp = await rpc('tools/call', {
|
|
227
|
+
name: 'backup_status',
|
|
228
|
+
arguments: { path: tmpDir },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
await test('backup_status returns structured status', () => {
|
|
232
|
+
const content = statusResp.result.content[0].text;
|
|
233
|
+
const data = JSON.parse(content);
|
|
234
|
+
if (typeof data.watcher !== 'object') throw new Error('no watcher');
|
|
235
|
+
if (typeof data.config !== 'object') throw new Error('no config');
|
|
236
|
+
if (typeof data.lastBackup !== 'object') throw new Error('no lastBackup');
|
|
237
|
+
if (typeof data.refs !== 'object') throw new Error('no refs');
|
|
238
|
+
if (typeof data.disk !== 'object') throw new Error('no disk');
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Call doctor_fix (dry-run)
|
|
242
|
+
const fixDryResp = await rpc('tools/call', {
|
|
243
|
+
name: 'doctor_fix',
|
|
244
|
+
arguments: { path: tmpDir, dry_run: true },
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
await test('doctor_fix dry-run returns actions without fixing', () => {
|
|
248
|
+
const content = fixDryResp.result.content[0].text;
|
|
249
|
+
const data = JSON.parse(content);
|
|
250
|
+
if (!Array.isArray(data.actions)) throw new Error('actions not an array');
|
|
251
|
+
if (data.totalFixed !== 0) throw new Error(`dry-run should not fix, but fixed ${data.totalFixed}`);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Call doctor_fix (apply)
|
|
255
|
+
const fixResp = await rpc('tools/call', {
|
|
256
|
+
name: 'doctor_fix',
|
|
257
|
+
arguments: { path: tmpDir },
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await test('doctor_fix returns structured result', () => {
|
|
261
|
+
const content = fixResp.result.content[0].text;
|
|
262
|
+
const data = JSON.parse(content);
|
|
263
|
+
if (!Array.isArray(data.actions)) throw new Error('actions not an array');
|
|
264
|
+
if (typeof data.totalFixed !== 'number') throw new Error('totalFixed missing');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Cleanup
|
|
268
|
+
serverProcess.kill();
|
|
269
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
270
|
+
|
|
271
|
+
console.log(`\n${passed + failed} tests: \x1b[32m${passed} passed\x1b[0m` + (failed ? `, \x1b[31m${failed} failed\x1b[0m` : ''));
|
|
272
|
+
process.exit(failed > 0 ? 1 : 0);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
run().catch(err => {
|
|
276
|
+
console.error('Test runner error:', err);
|
|
277
|
+
if (serverProcess) serverProcess.kill();
|
|
278
|
+
process.exit(1);
|
|
279
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
|
6
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
7
|
+
const { z } = require('zod');
|
|
8
|
+
|
|
9
|
+
const { runDiagnostics } = require('../lib/core/doctor');
|
|
10
|
+
const { createGitSnapshot, createShadowCopy } = require('../lib/core/snapshot');
|
|
11
|
+
const { listBackups } = require('../lib/core/backups');
|
|
12
|
+
const { restoreFile, previewProjectRestore, executeProjectRestore } = require('../lib/core/restore');
|
|
13
|
+
const { runFixes } = require('../lib/core/doctor-fix');
|
|
14
|
+
const { getBackupStatus } = require('../lib/core/status');
|
|
15
|
+
|
|
16
|
+
const pkg = require('../../package.json');
|
|
17
|
+
|
|
18
|
+
// ── Server ──────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const server = new McpServer({
|
|
21
|
+
name: 'cursor-guard',
|
|
22
|
+
version: pkg.version,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// ── Tool 1: doctor ──────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
server.tool(
|
|
28
|
+
'doctor',
|
|
29
|
+
'Run health checks on a project: environment, config, Git, backup refs, shadow copies, disk space. Read-only, safe to call anytime.',
|
|
30
|
+
{
|
|
31
|
+
path: z.string().describe('Absolute path to the project directory'),
|
|
32
|
+
},
|
|
33
|
+
async ({ path: projectPath }) => {
|
|
34
|
+
const resolved = path.resolve(projectPath);
|
|
35
|
+
const result = runDiagnostics(resolved);
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: 'text',
|
|
39
|
+
text: JSON.stringify(result, null, 2),
|
|
40
|
+
}],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// ── Tool 2: list_backups ────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
server.tool(
|
|
48
|
+
'list_backups',
|
|
49
|
+
'List available backup/restore points from all sources (git refs, shadow copies). Read-only. Use before restore to find candidate versions.',
|
|
50
|
+
{
|
|
51
|
+
path: z.string().describe('Absolute path to the project directory'),
|
|
52
|
+
file: z.string().optional().describe('Filter to a specific file (relative path)'),
|
|
53
|
+
before: z.string().optional().describe('Only show backups before this time (e.g. "10 minutes ago", "2026-03-21T14:00:00")'),
|
|
54
|
+
limit: z.number().optional().describe('Max results per source (default 20)'),
|
|
55
|
+
},
|
|
56
|
+
async ({ path: projectPath, file, before, limit }) => {
|
|
57
|
+
const resolved = path.resolve(projectPath);
|
|
58
|
+
const result = listBackups(resolved, { file, before, limit });
|
|
59
|
+
return {
|
|
60
|
+
content: [{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: JSON.stringify(result, null, 2),
|
|
63
|
+
}],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// ── Tool 3: snapshot_now ────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
server.tool(
|
|
71
|
+
'snapshot_now',
|
|
72
|
+
'Create an immediate backup snapshot of the current project state. Use before risky operations to preserve a restore point.',
|
|
73
|
+
{
|
|
74
|
+
path: z.string().describe('Absolute path to the project directory'),
|
|
75
|
+
strategy: z.enum(['git', 'shadow', 'both']).optional().describe('Backup strategy (default: from config, or "git")'),
|
|
76
|
+
message: z.string().optional().describe('Custom commit message for git snapshot'),
|
|
77
|
+
},
|
|
78
|
+
async ({ path: projectPath, strategy, message }) => {
|
|
79
|
+
const resolved = path.resolve(projectPath);
|
|
80
|
+
const { loadConfig } = require('../lib/utils');
|
|
81
|
+
const { cfg } = loadConfig(resolved);
|
|
82
|
+
|
|
83
|
+
const effectiveStrategy = strategy || cfg.backup_strategy || 'git';
|
|
84
|
+
const results = {};
|
|
85
|
+
|
|
86
|
+
if (effectiveStrategy === 'git' || effectiveStrategy === 'both') {
|
|
87
|
+
results.git = createGitSnapshot(resolved, cfg, {
|
|
88
|
+
branchRef: 'refs/guard/snapshot',
|
|
89
|
+
message: message || `guard: manual snapshot ${new Date().toISOString()}`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (effectiveStrategy === 'shadow' || effectiveStrategy === 'both') {
|
|
94
|
+
results.shadow = createShadowCopy(resolved, cfg);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
content: [{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: JSON.stringify(results, null, 2),
|
|
101
|
+
}],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// ── Tool 4: restore_file ────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
server.tool(
|
|
109
|
+
'restore_file',
|
|
110
|
+
'Restore a single file from a backup source (git commit/ref or shadow copy timestamp). By default, preserves the current version in a pre-restore snapshot before restoring.',
|
|
111
|
+
{
|
|
112
|
+
path: z.string().describe('Absolute path to the project directory'),
|
|
113
|
+
file: z.string().describe('Relative path to the file to restore'),
|
|
114
|
+
source: z.string().describe('Backup source: git commit hash, ref name, or shadow copy timestamp (e.g. "20260321_143205")'),
|
|
115
|
+
preserve_current: z.boolean().optional().describe('Create pre-restore snapshot before restoring (default true)'),
|
|
116
|
+
},
|
|
117
|
+
async ({ path: projectPath, file, source, preserve_current }) => {
|
|
118
|
+
const resolved = path.resolve(projectPath);
|
|
119
|
+
const result = restoreFile(resolved, file, source, {
|
|
120
|
+
preserveCurrent: preserve_current,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
content: [{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: JSON.stringify(result, null, 2),
|
|
126
|
+
}],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// ── Tool 5: restore_project ─────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
server.tool(
|
|
134
|
+
'restore_project',
|
|
135
|
+
'Preview or execute a full project restore to a given backup point. In preview mode (default), shows affected files without changes. In execute mode, creates a pre-restore snapshot then restores all files.',
|
|
136
|
+
{
|
|
137
|
+
path: z.string().describe('Absolute path to the project directory'),
|
|
138
|
+
source: z.string().describe('Backup source: git commit hash or ref name'),
|
|
139
|
+
preview: z.boolean().optional().describe('If true (default), only show what would change. If false, execute the restore.'),
|
|
140
|
+
preserve_current: z.boolean().optional().describe('Create pre-restore snapshot before executing (default true, only used when preview=false)'),
|
|
141
|
+
},
|
|
142
|
+
async ({ path: projectPath, source, preview, preserve_current }) => {
|
|
143
|
+
const resolved = path.resolve(projectPath);
|
|
144
|
+
|
|
145
|
+
if (preview !== false) {
|
|
146
|
+
const result = previewProjectRestore(resolved, source);
|
|
147
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const result = executeProjectRestore(resolved, source, {
|
|
151
|
+
preserveCurrent: preserve_current,
|
|
152
|
+
});
|
|
153
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// ── Tool 6: doctor_fix ──────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
server.tool(
|
|
160
|
+
'doctor_fix',
|
|
161
|
+
'Auto-fix common configuration and environment issues: create missing config, init git repo, add .cursor-guard-backup/ to .gitignore, remove stale lock files, fix strategy mismatch. Each fix is idempotent. Use dry_run=true to preview without changes.',
|
|
162
|
+
{
|
|
163
|
+
path: z.string().describe('Absolute path to the project directory'),
|
|
164
|
+
dry_run: z.boolean().optional().describe('If true, report what would be fixed without modifying anything (default false)'),
|
|
165
|
+
},
|
|
166
|
+
async ({ path: projectPath, dry_run }) => {
|
|
167
|
+
const resolved = path.resolve(projectPath);
|
|
168
|
+
const result = runFixes(resolved, { dryRun: !!dry_run });
|
|
169
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// ── Tool 7: backup_status ───────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
server.tool(
|
|
176
|
+
'backup_status',
|
|
177
|
+
'Get comprehensive backup system status: watcher running/stale, last backup time per strategy, configured strategy and retention, guard ref counts, disk space. Read-only, safe to call anytime.',
|
|
178
|
+
{
|
|
179
|
+
path: z.string().describe('Absolute path to the project directory'),
|
|
180
|
+
},
|
|
181
|
+
async ({ path: projectPath }) => {
|
|
182
|
+
const resolved = path.resolve(projectPath);
|
|
183
|
+
const result = getBackupStatus(resolved);
|
|
184
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// ── Start ───────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
async function main() {
|
|
191
|
+
const transport = new StdioServerTransport();
|
|
192
|
+
await server.connect(transport);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main().catch((err) => {
|
|
196
|
+
console.error('cursor-guard MCP server failed to start:', err);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|