@prmichaelsen/acp-mcp 0.7.1 → 1.0.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/CHANGELOG.md +57 -0
- package/README.md +121 -41
- package/agent/progress.yaml +29 -5
- package/dist/server-factory.js +2 -496
- package/dist/server-factory.js.map +4 -4
- package/dist/server.js +2 -496
- package/dist/server.js.map +4 -4
- package/dist/utils/ssh-connection.d.ts +0 -40
- package/package.json +1 -1
- package/src/server-factory.ts +2 -12
- package/src/server.ts +2 -11
- package/src/utils/ssh-connection.ts +1 -304
- package/dist/tools/acp-remote-list-files.d.ts +0 -16
- package/dist/tools/acp-remote-read-file.d.ts +0 -16
- package/dist/tools/acp-remote-write-file.d.ts +0 -16
- package/dist/types/file-entry.d.ts +0 -88
- package/src/tools/acp-remote-list-files.ts +0 -100
- package/src/tools/acp-remote-read-file.ts +0 -94
- package/src/tools/acp-remote-write-file.ts +0 -107
- package/src/types/file-entry.ts +0 -123
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Client
|
|
1
|
+
import { Client } from 'ssh2';
|
|
2
2
|
import { SSHConfig } from '../types/ssh-config.js';
|
|
3
|
-
import { FileEntry, parsePermissions, getFileType } from '../types/file-entry.js';
|
|
4
3
|
import { logger } from './logger.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -230,308 +229,6 @@ export class SSHConnectionManager {
|
|
|
230
229
|
});
|
|
231
230
|
}
|
|
232
231
|
|
|
233
|
-
/**
|
|
234
|
-
* Get SFTP wrapper for file operations
|
|
235
|
-
*/
|
|
236
|
-
async getSFTP(): Promise<SFTPWrapper> {
|
|
237
|
-
if (!this.connected) {
|
|
238
|
-
await this.connect();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return new Promise((resolve, reject) => {
|
|
242
|
-
this.client.sftp((err, sftp) => {
|
|
243
|
-
if (err) {
|
|
244
|
-
reject(err);
|
|
245
|
-
} else {
|
|
246
|
-
resolve(sftp);
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* List files in a directory with comprehensive metadata
|
|
254
|
-
* Uses hybrid approach: shell ls for filenames (includes hidden), SFTP stat for metadata
|
|
255
|
-
*
|
|
256
|
-
* @param path - Directory path to list
|
|
257
|
-
* @param includeHidden - Whether to include hidden files (default: true)
|
|
258
|
-
* @returns Array of FileEntry objects with complete metadata
|
|
259
|
-
*/
|
|
260
|
-
async listFiles(path: string, includeHidden: boolean = true): Promise<FileEntry[]> {
|
|
261
|
-
const startTime = Date.now();
|
|
262
|
-
logger.debug('Listing files', { path, includeHidden });
|
|
263
|
-
|
|
264
|
-
try {
|
|
265
|
-
// Step 1: Use shell command to get ALL filenames (including hidden)
|
|
266
|
-
const lsFlag = includeHidden ? '-A' : '';
|
|
267
|
-
const command = `ls ${lsFlag} -1 "${path}" 2>/dev/null`;
|
|
268
|
-
const result = await this.execWithTimeout(command, 10);
|
|
269
|
-
|
|
270
|
-
if (result.exitCode !== 0) {
|
|
271
|
-
throw new Error(`ls command failed: ${result.stderr}`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const filenames = result.stdout
|
|
275
|
-
.split('\n')
|
|
276
|
-
.map(f => f.trim())
|
|
277
|
-
.filter(f => f !== '' && f !== '.' && f !== '..');
|
|
278
|
-
|
|
279
|
-
logger.debug('Filenames retrieved via shell', {
|
|
280
|
-
path,
|
|
281
|
-
count: filenames.length,
|
|
282
|
-
method: 'shell',
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// Step 2: Get rich metadata for each file using SFTP stat()
|
|
286
|
-
const sftp = await this.getSFTP();
|
|
287
|
-
const entries: FileEntry[] = [];
|
|
288
|
-
|
|
289
|
-
for (const filename of filenames) {
|
|
290
|
-
const fullPath = `${path}/${filename}`.replace(/\/+/g, '/');
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
const stats = await new Promise<any>((resolve, reject) => {
|
|
294
|
-
sftp.stat(fullPath, (err, stats) => {
|
|
295
|
-
if (err) reject(err);
|
|
296
|
-
else resolve(stats);
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
entries.push({
|
|
301
|
-
name: filename,
|
|
302
|
-
path: fullPath,
|
|
303
|
-
type: getFileType(stats),
|
|
304
|
-
size: stats.size,
|
|
305
|
-
permissions: parsePermissions(stats.mode),
|
|
306
|
-
owner: {
|
|
307
|
-
uid: stats.uid,
|
|
308
|
-
gid: stats.gid,
|
|
309
|
-
},
|
|
310
|
-
timestamps: {
|
|
311
|
-
accessed: new Date(stats.atime * 1000).toISOString(),
|
|
312
|
-
modified: new Date(stats.mtime * 1000).toISOString(),
|
|
313
|
-
},
|
|
314
|
-
});
|
|
315
|
-
} catch (error) {
|
|
316
|
-
// Skip files we can't stat (permissions, race conditions, etc.)
|
|
317
|
-
logger.warn('Failed to stat file, skipping', {
|
|
318
|
-
path: fullPath,
|
|
319
|
-
error: error instanceof Error ? error.message : String(error),
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const duration = Date.now() - startTime;
|
|
325
|
-
logger.debug('Files listed successfully', {
|
|
326
|
-
path,
|
|
327
|
-
count: entries.length,
|
|
328
|
-
duration: `${duration}ms`,
|
|
329
|
-
method: 'hybrid',
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
return entries;
|
|
333
|
-
} catch (error) {
|
|
334
|
-
// Fallback to SFTP readdir if shell command fails
|
|
335
|
-
logger.warn('Shell ls command failed, falling back to SFTP readdir', {
|
|
336
|
-
path,
|
|
337
|
-
error: error instanceof Error ? error.message : String(error),
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
return this.listFilesViaSFTP(path, includeHidden);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Fallback method: List files using SFTP readdir (may miss hidden files)
|
|
346
|
-
* @private
|
|
347
|
-
*/
|
|
348
|
-
private async listFilesViaSFTP(path: string, includeHidden: boolean): Promise<FileEntry[]> {
|
|
349
|
-
const sftp = await this.getSFTP();
|
|
350
|
-
|
|
351
|
-
return new Promise((resolve, reject) => {
|
|
352
|
-
sftp.readdir(path, (err, list) => {
|
|
353
|
-
if (err) {
|
|
354
|
-
logger.error('SFTP readdir failed', { path, error: err.message });
|
|
355
|
-
reject(err);
|
|
356
|
-
return;
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
let entries = list.map((item) => ({
|
|
360
|
-
name: item.filename,
|
|
361
|
-
path: `${path}/${item.filename}`.replace(/\/+/g, '/'),
|
|
362
|
-
type: getFileType(item.attrs),
|
|
363
|
-
size: item.attrs.size,
|
|
364
|
-
permissions: parsePermissions(item.attrs.mode),
|
|
365
|
-
owner: {
|
|
366
|
-
uid: item.attrs.uid,
|
|
367
|
-
gid: item.attrs.gid,
|
|
368
|
-
},
|
|
369
|
-
timestamps: {
|
|
370
|
-
accessed: new Date(item.attrs.atime * 1000).toISOString(),
|
|
371
|
-
modified: new Date(item.attrs.mtime * 1000).toISOString(),
|
|
372
|
-
},
|
|
373
|
-
}));
|
|
374
|
-
|
|
375
|
-
// SFTP readdir doesn't return hidden files, so filter if requested
|
|
376
|
-
if (!includeHidden) {
|
|
377
|
-
entries = entries.filter(e => !e.name.startsWith('.'));
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
logger.debug('Files listed via SFTP fallback', {
|
|
381
|
-
path,
|
|
382
|
-
count: entries.length,
|
|
383
|
-
method: 'sftp',
|
|
384
|
-
note: 'Hidden files may be missing (SFTP limitation)',
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
resolve(entries);
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Read file contents from remote machine
|
|
394
|
-
*/
|
|
395
|
-
async readFile(
|
|
396
|
-
path: string,
|
|
397
|
-
encoding: string = 'utf-8',
|
|
398
|
-
maxSize: number = 1048576
|
|
399
|
-
): Promise<{ content: string; size: number; encoding: string }> {
|
|
400
|
-
const startTime = Date.now();
|
|
401
|
-
logger.fileOperation('read', path, { encoding, maxSize });
|
|
402
|
-
|
|
403
|
-
const sftp = await this.getSFTP();
|
|
404
|
-
|
|
405
|
-
return new Promise((resolve, reject) => {
|
|
406
|
-
// First, get file stats to check size
|
|
407
|
-
sftp.stat(path, (err, stats) => {
|
|
408
|
-
if (err) {
|
|
409
|
-
logger.error('File stat failed', { path, error: err.message });
|
|
410
|
-
reject(new Error(`File not found or inaccessible: ${path}`));
|
|
411
|
-
return;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
logger.debug('File stat retrieved', { path, size: stats.size });
|
|
415
|
-
|
|
416
|
-
if (stats.size > maxSize) {
|
|
417
|
-
logger.warn('File too large', { path, size: stats.size, maxSize });
|
|
418
|
-
reject(new Error(`File too large: ${stats.size} bytes (max: ${maxSize} bytes)`));
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// Read file contents
|
|
423
|
-
sftp.readFile(path, { encoding: encoding as BufferEncoding }, (err, data) => {
|
|
424
|
-
if (err) {
|
|
425
|
-
logger.error('File read failed', { path, error: err.message });
|
|
426
|
-
reject(new Error(`Failed to read file: ${err.message}`));
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const duration = Date.now() - startTime;
|
|
431
|
-
logger.debug('File read completed', { path, size: stats.size, duration: `${duration}ms` });
|
|
432
|
-
|
|
433
|
-
resolve({
|
|
434
|
-
content: data.toString(),
|
|
435
|
-
size: stats.size,
|
|
436
|
-
encoding,
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* Write file contents to remote machine
|
|
445
|
-
*/
|
|
446
|
-
async writeFile(
|
|
447
|
-
path: string,
|
|
448
|
-
content: string,
|
|
449
|
-
options: {
|
|
450
|
-
encoding?: string;
|
|
451
|
-
createDirs?: boolean;
|
|
452
|
-
backup?: boolean;
|
|
453
|
-
} = {}
|
|
454
|
-
): Promise<{ success: boolean; bytesWritten: number; backupPath?: string }> {
|
|
455
|
-
const { encoding = 'utf-8', createDirs = false, backup = false } = options;
|
|
456
|
-
const startTime = Date.now();
|
|
457
|
-
|
|
458
|
-
logger.fileOperation('write', path, {
|
|
459
|
-
contentSize: content.length,
|
|
460
|
-
encoding,
|
|
461
|
-
createDirs,
|
|
462
|
-
backup,
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
const sftp = await this.getSFTP();
|
|
466
|
-
|
|
467
|
-
return new Promise((resolve, reject) => {
|
|
468
|
-
const writeOperation = () => {
|
|
469
|
-
// Create backup if requested
|
|
470
|
-
if (backup) {
|
|
471
|
-
const backupPath = `${path}.backup`;
|
|
472
|
-
sftp.rename(path, backupPath, (err) => {
|
|
473
|
-
if (err && err.message !== 'No such file') {
|
|
474
|
-
// Ignore "no such file" error (file doesn't exist yet)
|
|
475
|
-
reject(new Error(`Failed to create backup: ${err.message}`));
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Write file
|
|
480
|
-
performWrite(backupPath);
|
|
481
|
-
});
|
|
482
|
-
} else {
|
|
483
|
-
performWrite();
|
|
484
|
-
}
|
|
485
|
-
};
|
|
486
|
-
|
|
487
|
-
const performWrite = (backupPath?: string) => {
|
|
488
|
-
const buffer = Buffer.from(content, encoding as BufferEncoding);
|
|
489
|
-
const tempPath = `${path}.tmp`;
|
|
490
|
-
|
|
491
|
-
// Write to temp file first (atomic write)
|
|
492
|
-
sftp.writeFile(tempPath, buffer, (err) => {
|
|
493
|
-
if (err) {
|
|
494
|
-
reject(new Error(`Failed to write file: ${err.message}`));
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// Rename temp file to target (atomic operation)
|
|
499
|
-
sftp.rename(tempPath, path, (err) => {
|
|
500
|
-
if (err) {
|
|
501
|
-
logger.error('File rename failed', { tempPath, path, error: err.message });
|
|
502
|
-
reject(new Error(`Failed to rename temp file: ${err.message}`));
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
const duration = Date.now() - startTime;
|
|
507
|
-
logger.debug('File write completed', {
|
|
508
|
-
path,
|
|
509
|
-
bytesWritten: buffer.length,
|
|
510
|
-
duration: `${duration}ms`,
|
|
511
|
-
backupPath,
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
resolve({
|
|
515
|
-
success: true,
|
|
516
|
-
bytesWritten: buffer.length,
|
|
517
|
-
backupPath,
|
|
518
|
-
});
|
|
519
|
-
});
|
|
520
|
-
});
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// Create parent directories if requested
|
|
524
|
-
if (createDirs) {
|
|
525
|
-
const dirPath = path.substring(0, path.lastIndexOf('/'));
|
|
526
|
-
this.exec(`mkdir -p ${dirPath}`).then(() => {
|
|
527
|
-
writeOperation();
|
|
528
|
-
}).catch(reject);
|
|
529
|
-
} else {
|
|
530
|
-
writeOperation();
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
|
-
|
|
535
232
|
/**
|
|
536
233
|
* Wrap command to source shell configuration files
|
|
537
234
|
* This ensures PATH and other environment variables are properly set
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { SSHConnectionManager } from '../utils/ssh-connection.js';
|
|
3
|
-
export declare const acpRemoteListFilesTool: Tool;
|
|
4
|
-
/**
|
|
5
|
-
* Handle the acp_remote_list_files tool invocation
|
|
6
|
-
* Lists files and directories at the specified path on the remote machine via SSH
|
|
7
|
-
*
|
|
8
|
-
* @param args - Tool arguments containing path and recursive flag
|
|
9
|
-
* @param sshConnection - SSH connection manager for remote operations
|
|
10
|
-
*/
|
|
11
|
-
export declare function handleAcpRemoteListFiles(args: any, sshConnection: SSHConnectionManager): Promise<{
|
|
12
|
-
content: Array<{
|
|
13
|
-
type: string;
|
|
14
|
-
text: string;
|
|
15
|
-
}>;
|
|
16
|
-
}>;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { SSHConnectionManager } from '../utils/ssh-connection.js';
|
|
3
|
-
export declare const acpRemoteReadFileTool: Tool;
|
|
4
|
-
/**
|
|
5
|
-
* Handle the acp_remote_read_file tool invocation
|
|
6
|
-
* Reads file contents from the remote machine via SSH
|
|
7
|
-
*
|
|
8
|
-
* @param args - Tool arguments containing path, encoding, and maxSize
|
|
9
|
-
* @param sshConnection - SSH connection manager for remote operations
|
|
10
|
-
*/
|
|
11
|
-
export declare function handleAcpRemoteReadFile(args: any, sshConnection: SSHConnectionManager): Promise<{
|
|
12
|
-
content: Array<{
|
|
13
|
-
type: string;
|
|
14
|
-
text: string;
|
|
15
|
-
}>;
|
|
16
|
-
}>;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { SSHConnectionManager } from '../utils/ssh-connection.js';
|
|
3
|
-
export declare const acpRemoteWriteFileTool: Tool;
|
|
4
|
-
/**
|
|
5
|
-
* Handle the acp_remote_write_file tool invocation
|
|
6
|
-
* Writes file contents to the remote machine via SSH
|
|
7
|
-
*
|
|
8
|
-
* @param args - Tool arguments containing path, content, and options
|
|
9
|
-
* @param sshConnection - SSH connection manager for remote operations
|
|
10
|
-
*/
|
|
11
|
-
export declare function handleAcpRemoteWriteFile(args: any, sshConnection: SSHConnectionManager): Promise<{
|
|
12
|
-
content: Array<{
|
|
13
|
-
type: string;
|
|
14
|
-
text: string;
|
|
15
|
-
}>;
|
|
16
|
-
}>;
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Comprehensive file entry with metadata
|
|
3
|
-
*/
|
|
4
|
-
export interface FileEntry {
|
|
5
|
-
/** Filename (without path) */
|
|
6
|
-
name: string;
|
|
7
|
-
/** Absolute path to file */
|
|
8
|
-
path: string;
|
|
9
|
-
/** File type */
|
|
10
|
-
type: 'file' | 'directory' | 'symlink' | 'other';
|
|
11
|
-
/** File size in bytes */
|
|
12
|
-
size: number;
|
|
13
|
-
/** File permissions */
|
|
14
|
-
permissions: {
|
|
15
|
-
/** Octal mode (e.g., 0o644) */
|
|
16
|
-
mode: number;
|
|
17
|
-
/** Human-readable string (e.g., "rw-r--r--") */
|
|
18
|
-
string: string;
|
|
19
|
-
/** Owner permissions */
|
|
20
|
-
owner: {
|
|
21
|
-
read: boolean;
|
|
22
|
-
write: boolean;
|
|
23
|
-
execute: boolean;
|
|
24
|
-
};
|
|
25
|
-
/** Group permissions */
|
|
26
|
-
group: {
|
|
27
|
-
read: boolean;
|
|
28
|
-
write: boolean;
|
|
29
|
-
execute: boolean;
|
|
30
|
-
};
|
|
31
|
-
/** Others permissions */
|
|
32
|
-
others: {
|
|
33
|
-
read: boolean;
|
|
34
|
-
write: boolean;
|
|
35
|
-
execute: boolean;
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
/** File ownership */
|
|
39
|
-
owner: {
|
|
40
|
-
/** User ID */
|
|
41
|
-
uid: number;
|
|
42
|
-
/** Group ID */
|
|
43
|
-
gid: number;
|
|
44
|
-
};
|
|
45
|
-
/** File timestamps */
|
|
46
|
-
timestamps: {
|
|
47
|
-
/** Last access time (ISO 8601) */
|
|
48
|
-
accessed: string;
|
|
49
|
-
/** Last modification time (ISO 8601) */
|
|
50
|
-
modified: string;
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Convert Unix mode to human-readable permission string
|
|
55
|
-
* @param mode - Unix file mode (e.g., 33188 for -rw-r--r--)
|
|
56
|
-
* @returns Permission string (e.g., "rw-r--r--")
|
|
57
|
-
*/
|
|
58
|
-
export declare function modeToPermissionString(mode: number): string;
|
|
59
|
-
/**
|
|
60
|
-
* Parse Unix mode into structured permissions object
|
|
61
|
-
* @param mode - Unix file mode
|
|
62
|
-
* @returns Structured permissions object
|
|
63
|
-
*/
|
|
64
|
-
export declare function parsePermissions(mode: number): {
|
|
65
|
-
mode: number;
|
|
66
|
-
string: string;
|
|
67
|
-
owner: {
|
|
68
|
-
read: boolean;
|
|
69
|
-
write: boolean;
|
|
70
|
-
execute: boolean;
|
|
71
|
-
};
|
|
72
|
-
group: {
|
|
73
|
-
read: boolean;
|
|
74
|
-
write: boolean;
|
|
75
|
-
execute: boolean;
|
|
76
|
-
};
|
|
77
|
-
others: {
|
|
78
|
-
read: boolean;
|
|
79
|
-
write: boolean;
|
|
80
|
-
execute: boolean;
|
|
81
|
-
};
|
|
82
|
-
};
|
|
83
|
-
/**
|
|
84
|
-
* Determine file type from SFTP stats
|
|
85
|
-
* @param stats - SFTP file stats
|
|
86
|
-
* @returns File type string
|
|
87
|
-
*/
|
|
88
|
-
export declare function getFileType(stats: any): 'file' | 'directory' | 'symlink' | 'other';
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { SSHConnectionManager } from '../utils/ssh-connection.js';
|
|
3
|
-
import { FileEntry } from '../types/file-entry.js';
|
|
4
|
-
|
|
5
|
-
export const acpRemoteListFilesTool: Tool = {
|
|
6
|
-
name: 'acp_remote_list_files',
|
|
7
|
-
description: 'List files and directories in a specified path on the remote machine via SSH. Returns comprehensive metadata including permissions, timestamps, size, and ownership. Includes hidden files by default.',
|
|
8
|
-
inputSchema: {
|
|
9
|
-
type: 'object',
|
|
10
|
-
properties: {
|
|
11
|
-
path: {
|
|
12
|
-
type: 'string',
|
|
13
|
-
description: 'The directory path to list files from',
|
|
14
|
-
},
|
|
15
|
-
recursive: {
|
|
16
|
-
type: 'boolean',
|
|
17
|
-
description: 'Whether to list files recursively',
|
|
18
|
-
default: false,
|
|
19
|
-
},
|
|
20
|
-
includeHidden: {
|
|
21
|
-
type: 'boolean',
|
|
22
|
-
description: 'Whether to include hidden files (starting with .)',
|
|
23
|
-
default: true,
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
required: ['path'],
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
interface ListFilesArgs {
|
|
31
|
-
path: string;
|
|
32
|
-
recursive?: boolean;
|
|
33
|
-
includeHidden?: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Handle the acp_remote_list_files tool invocation
|
|
38
|
-
* Lists files and directories at the specified path on the remote machine via SSH
|
|
39
|
-
*
|
|
40
|
-
* @param args - Tool arguments containing path and recursive flag
|
|
41
|
-
* @param sshConnection - SSH connection manager for remote operations
|
|
42
|
-
*/
|
|
43
|
-
export async function handleAcpRemoteListFiles(
|
|
44
|
-
args: any,
|
|
45
|
-
sshConnection: SSHConnectionManager
|
|
46
|
-
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
47
|
-
const { path, recursive = false, includeHidden = true } = args as ListFilesArgs;
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
const entries = await listRemoteFiles(sshConnection, path, recursive, includeHidden);
|
|
51
|
-
|
|
52
|
-
// Format as JSON for structured output
|
|
53
|
-
const output = JSON.stringify(entries, null, 2);
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
content: [
|
|
57
|
-
{
|
|
58
|
-
type: 'text',
|
|
59
|
-
text: output,
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
};
|
|
63
|
-
} catch (error) {
|
|
64
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
65
|
-
return {
|
|
66
|
-
content: [
|
|
67
|
-
{
|
|
68
|
-
type: 'text',
|
|
69
|
-
text: `Error listing remote files: ${errorMessage}`,
|
|
70
|
-
},
|
|
71
|
-
],
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Recursively list files in a remote directory via SSH
|
|
78
|
-
* Returns FileEntry objects with comprehensive metadata
|
|
79
|
-
*/
|
|
80
|
-
async function listRemoteFiles(
|
|
81
|
-
ssh: SSHConnectionManager,
|
|
82
|
-
dirPath: string,
|
|
83
|
-
recursive: boolean,
|
|
84
|
-
includeHidden: boolean
|
|
85
|
-
): Promise<FileEntry[]> {
|
|
86
|
-
const entries = await ssh.listFiles(dirPath, includeHidden);
|
|
87
|
-
const allEntries: FileEntry[] = [...entries];
|
|
88
|
-
|
|
89
|
-
// Recursively list subdirectories if requested
|
|
90
|
-
if (recursive) {
|
|
91
|
-
for (const entry of entries) {
|
|
92
|
-
if (entry.type === 'directory') {
|
|
93
|
-
const subEntries = await listRemoteFiles(ssh, entry.path, recursive, includeHidden);
|
|
94
|
-
allEntries.push(...subEntries);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return allEntries;
|
|
100
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
import { SSHConnectionManager } from '../utils/ssh-connection.js';
|
|
3
|
-
import { logger } from '../utils/logger.js';
|
|
4
|
-
|
|
5
|
-
export const acpRemoteReadFileTool: Tool = {
|
|
6
|
-
name: 'acp_remote_read_file',
|
|
7
|
-
description: 'Read file contents from the remote machine via SSH',
|
|
8
|
-
inputSchema: {
|
|
9
|
-
type: 'object',
|
|
10
|
-
properties: {
|
|
11
|
-
path: {
|
|
12
|
-
type: 'string',
|
|
13
|
-
description: 'Absolute path to file',
|
|
14
|
-
},
|
|
15
|
-
encoding: {
|
|
16
|
-
type: 'string',
|
|
17
|
-
description: 'File encoding (default: utf-8)',
|
|
18
|
-
default: 'utf-8',
|
|
19
|
-
enum: ['utf-8', 'ascii', 'base64'],
|
|
20
|
-
},
|
|
21
|
-
maxSize: {
|
|
22
|
-
type: 'number',
|
|
23
|
-
description: 'Max file size in bytes (default: 1MB)',
|
|
24
|
-
default: 1048576,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
required: ['path'],
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
interface ReadFileArgs {
|
|
32
|
-
path: string;
|
|
33
|
-
encoding?: string;
|
|
34
|
-
maxSize?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface ReadFileResult {
|
|
38
|
-
content: string;
|
|
39
|
-
size: number;
|
|
40
|
-
encoding: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Handle the acp_remote_read_file tool invocation
|
|
45
|
-
* Reads file contents from the remote machine via SSH
|
|
46
|
-
*
|
|
47
|
-
* @param args - Tool arguments containing path, encoding, and maxSize
|
|
48
|
-
* @param sshConnection - SSH connection manager for remote operations
|
|
49
|
-
*/
|
|
50
|
-
export async function handleAcpRemoteReadFile(
|
|
51
|
-
args: any,
|
|
52
|
-
sshConnection: SSHConnectionManager
|
|
53
|
-
): Promise<{ content: Array<{ type: string; text: string }> }> {
|
|
54
|
-
const { path, encoding = 'utf-8', maxSize = 1048576 } = args as ReadFileArgs;
|
|
55
|
-
|
|
56
|
-
logger.debug('Reading remote file', { path, encoding, maxSize });
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
const result = await sshConnection.readFile(path, encoding, maxSize);
|
|
60
|
-
|
|
61
|
-
logger.debug('File read successful', { path, size: result.size });
|
|
62
|
-
|
|
63
|
-
const output: ReadFileResult = {
|
|
64
|
-
content: result.content,
|
|
65
|
-
size: result.size,
|
|
66
|
-
encoding: result.encoding,
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
content: [
|
|
71
|
-
{
|
|
72
|
-
type: 'text',
|
|
73
|
-
text: JSON.stringify(output, null, 2),
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
};
|
|
77
|
-
} catch (error) {
|
|
78
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
79
|
-
logger.error('File read error', { path, error: errorMessage });
|
|
80
|
-
return {
|
|
81
|
-
content: [
|
|
82
|
-
{
|
|
83
|
-
type: 'text',
|
|
84
|
-
text: JSON.stringify({
|
|
85
|
-
error: errorMessage,
|
|
86
|
-
content: '',
|
|
87
|
-
size: 0,
|
|
88
|
-
encoding,
|
|
89
|
-
}, null, 2),
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
}
|