silicon-transfer-mcp-server 0.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/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/index.js +45 -0
- package/dist/services/connection.js +175 -0
- package/dist/services/proof.js +46 -0
- package/dist/tools/register-dir.js +148 -0
- package/dist/tools/register.js +172 -0
- package/package.json +39 -0
- package/src/index.ts +55 -0
- package/src/services/connection.ts +180 -0
- package/src/services/proof.ts +64 -0
- package/src/tools/register-dir.ts +173 -0
- package/src/tools/register.ts +230 -0
- package/tsconfig.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Silicon Networks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Silicon Transfer MCP Server
|
|
2
|
+
|
|
3
|
+
**FTP / FTPS / SFTP for Claude and other MCP clients - with built-in transfer proofs.**
|
|
4
|
+
|
|
5
|
+
Every upload and download returns a verdict: `PROVEN` only when the SHA-256 of the
|
|
6
|
+
remote file matches your local file. No more "transfer complete" on blind trust.
|
|
7
|
+
|
|
8
|
+
> Philosophy: *prove it, then claim it.* Built from scratch by
|
|
9
|
+
> [Silicon Networks](https://aegis888.com) - the makers of AEGIS Shield.
|
|
10
|
+
|
|
11
|
+
## Why this one?
|
|
12
|
+
|
|
13
|
+
| | Typical FTP tools | Silicon Transfer |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| Upload result | "done" | verdict + SHA-256 of both sides |
|
|
16
|
+
| Directory sync check | manual | per-file proof walk with mismatch list |
|
|
17
|
+
| Remote file fingerprint | download first | streamed SHA-256, no disk contact |
|
|
18
|
+
| Protocols | often FTP only | FTP, FTPS, SFTP (password or SSH key) |
|
|
19
|
+
|
|
20
|
+
## Tools (12)
|
|
21
|
+
|
|
22
|
+
**Connection:** `silicon_connect`, `silicon_disconnect`, `silicon_status`, `silicon_server_info`
|
|
23
|
+
**Files:** `silicon_upload_file`, `silicon_download_file`, `silicon_delete_file`, `silicon_file_info`
|
|
24
|
+
**Directories:** `silicon_list_dir`, `silicon_make_dir`, `silicon_upload_dir`, `silicon_download_dir`, `silicon_rename`
|
|
25
|
+
|
|
26
|
+
Transfers accept `proof: "hash" | "size" | "none"` - hash is the default for single
|
|
27
|
+
files, size for directory trees (switch to hash when it matters).
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/SiliconAINetworks/silicon-transfer-mcp-server.git
|
|
33
|
+
cd silicon-transfer-mcp-server
|
|
34
|
+
npm install
|
|
35
|
+
npm run build
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Claude Desktop configuration
|
|
39
|
+
|
|
40
|
+
Add to your `claude_desktop_config.json` (no credentials in the config - you pass
|
|
41
|
+
them at runtime through `silicon_connect`, for **your own server**):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"mcpServers": {
|
|
46
|
+
"silicon-transfer": {
|
|
47
|
+
"command": "node",
|
|
48
|
+
"args": ["C:/path/to/silicon-transfer-mcp-server/dist/index.js"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Usage example
|
|
55
|
+
|
|
56
|
+
Ask your AI assistant:
|
|
57
|
+
|
|
58
|
+
> "Connect to my server via SFTP (host example.com, user deploy, key at ~/.ssh/id_ed25519)
|
|
59
|
+
> and upload dist/app.js to /var/www/app.js - with hash proof."
|
|
60
|
+
|
|
61
|
+
The assistant calls `silicon_connect`, then `silicon_upload_file` and answers with the
|
|
62
|
+
verdict:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"proof": {
|
|
67
|
+
"verdict": "PROVEN",
|
|
68
|
+
"detail": "SHA-256 identisch - Transfer bewiesen.",
|
|
69
|
+
"local_sha256": "9f2a...",
|
|
70
|
+
"remote_sha256": "9f2a..."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
If the hashes ever differ you get `MISMATCH` with both fingerprints - retry instead
|
|
76
|
+
of trusting a broken deploy.
|
|
77
|
+
|
|
78
|
+
## Security notes
|
|
79
|
+
|
|
80
|
+
- Credentials are **runtime-only tool parameters** for your own server. This project
|
|
81
|
+
ships no server addresses, no defaults, no config files with secrets.
|
|
82
|
+
- SFTP supports password or private key (`privateKeyPath`).
|
|
83
|
+
- `silicon_delete_file` is destructive - list first, delete second.
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT (c) 2026 Silicon Networks. Built from scratch - single-author codebase.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Silicon Transfer MCP Server
|
|
4
|
+
* FTP/SFTP/FTPS fuer Claude - mit eingebauten Transfer-Beweisen.
|
|
5
|
+
* Grundsatz: erst beweisen, dann behaupten. Jeder Upload liefert seinen Hash-Beweis mit.
|
|
6
|
+
*
|
|
7
|
+
* Built from scratch by Silicon Networks (aegis888.com), 2026.
|
|
8
|
+
*/
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import { ConnectionManager } from "./services/connection.js";
|
|
12
|
+
import { registerAll } from "./tools/register.js";
|
|
13
|
+
import { registerDirTools } from "./tools/register-dir.js";
|
|
14
|
+
const SERVER_NAME = "silicon-transfer-mcp-server";
|
|
15
|
+
const SERVER_VERSION = "0.1.0";
|
|
16
|
+
const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
|
|
17
|
+
const connections = new ConnectionManager();
|
|
18
|
+
server.registerTool("silicon_server_info", {
|
|
19
|
+
title: "Server Info",
|
|
20
|
+
description: "Name, Version und Philosophie dieses MCP-Servers. Lebenszeichen-Check.",
|
|
21
|
+
inputSchema: {},
|
|
22
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
23
|
+
}, async () => {
|
|
24
|
+
const output = {
|
|
25
|
+
name: SERVER_NAME,
|
|
26
|
+
version: SERVER_VERSION,
|
|
27
|
+
philosophy: "erst beweisen, dann behaupten - every transfer returns its checksum proof",
|
|
28
|
+
protocols: ["ftp", "ftps", "sftp"],
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
32
|
+
structuredContent: output,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
registerAll(server, connections);
|
|
36
|
+
registerDirTools(server, connections);
|
|
37
|
+
async function main() {
|
|
38
|
+
const transport = new StdioServerTransport();
|
|
39
|
+
await server.connect(transport);
|
|
40
|
+
console.error(`${SERVER_NAME} ${SERVER_VERSION} bereit (stdio)`);
|
|
41
|
+
}
|
|
42
|
+
main().catch((err) => {
|
|
43
|
+
console.error("Serverstart fehlgeschlagen:", err);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
});
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verbindungs-Schicht: FTP / FTPS / SFTP hinter einer einheitlichen Fassade.
|
|
3
|
+
* Ein aktiver Kanal pro Server-Instanz - bewusst simpel, bewusst nachvollziehbar.
|
|
4
|
+
*/
|
|
5
|
+
import { Client as FtpClient } from "basic-ftp";
|
|
6
|
+
import SftpClient from "ssh2-sftp-client";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { Writable } from "node:stream";
|
|
10
|
+
export class ConnectionManager {
|
|
11
|
+
ftp = null;
|
|
12
|
+
sftp = null;
|
|
13
|
+
opts = null;
|
|
14
|
+
get connected() {
|
|
15
|
+
return this.ftp !== null || this.sftp !== null;
|
|
16
|
+
}
|
|
17
|
+
get target() {
|
|
18
|
+
if (!this.opts)
|
|
19
|
+
return "nicht verbunden";
|
|
20
|
+
const p = this.opts.port ?? (this.opts.protocol === "sftp" ? 22 : 21);
|
|
21
|
+
return `${this.opts.protocol}://${this.opts.username}@${this.opts.host}:${p}`;
|
|
22
|
+
}
|
|
23
|
+
async connect(o) {
|
|
24
|
+
await this.disconnect();
|
|
25
|
+
if (o.protocol === "sftp") {
|
|
26
|
+
const c = new SftpClient();
|
|
27
|
+
await c.connect({
|
|
28
|
+
host: o.host,
|
|
29
|
+
port: o.port ?? 22,
|
|
30
|
+
username: o.username,
|
|
31
|
+
password: o.password,
|
|
32
|
+
privateKey: o.privateKeyPath ? readFileSync(o.privateKeyPath) : undefined,
|
|
33
|
+
passphrase: o.passphrase,
|
|
34
|
+
readyTimeout: 30000,
|
|
35
|
+
});
|
|
36
|
+
this.sftp = c;
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const c = new FtpClient();
|
|
40
|
+
await c.access({
|
|
41
|
+
host: o.host,
|
|
42
|
+
port: o.port ?? 21,
|
|
43
|
+
user: o.username,
|
|
44
|
+
password: o.password,
|
|
45
|
+
secure: o.protocol === "ftps",
|
|
46
|
+
});
|
|
47
|
+
this.ftp = c;
|
|
48
|
+
}
|
|
49
|
+
this.opts = o;
|
|
50
|
+
}
|
|
51
|
+
async disconnect() {
|
|
52
|
+
if (this.ftp) {
|
|
53
|
+
this.ftp.close();
|
|
54
|
+
this.ftp = null;
|
|
55
|
+
}
|
|
56
|
+
if (this.sftp) {
|
|
57
|
+
await this.sftp.end().catch(() => undefined);
|
|
58
|
+
this.sftp = null;
|
|
59
|
+
}
|
|
60
|
+
this.opts = null;
|
|
61
|
+
}
|
|
62
|
+
ensure() {
|
|
63
|
+
if (!this.connected) {
|
|
64
|
+
throw new Error("Keine aktive Verbindung - zuerst silicon_connect aufrufen.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async list(path) {
|
|
68
|
+
this.ensure();
|
|
69
|
+
if (this.sftp) {
|
|
70
|
+
const entries = await this.sftp.list(path);
|
|
71
|
+
return entries.map((e) => ({
|
|
72
|
+
name: e.name,
|
|
73
|
+
type: e.type === "d" ? "dir" : e.type === "l" ? "link" : "file",
|
|
74
|
+
size: e.size,
|
|
75
|
+
modifiedAt: new Date(e.modifyTime).toISOString(),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
const entries = await this.ftp.list(path);
|
|
79
|
+
return entries.map((e) => ({
|
|
80
|
+
name: e.name,
|
|
81
|
+
type: e.isDirectory ? "dir" : e.isSymbolicLink ? "link" : "file",
|
|
82
|
+
size: e.size,
|
|
83
|
+
modifiedAt: e.modifiedAt ? e.modifiedAt.toISOString() : undefined,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
async upload(localPath, remotePath) {
|
|
87
|
+
this.ensure();
|
|
88
|
+
if (this.sftp) {
|
|
89
|
+
await this.sftp.fastPut(localPath, remotePath);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await this.ftp.uploadFrom(localPath, remotePath);
|
|
93
|
+
}
|
|
94
|
+
async download(remotePath, localPath) {
|
|
95
|
+
this.ensure();
|
|
96
|
+
if (this.sftp) {
|
|
97
|
+
await this.sftp.fastGet(remotePath, localPath);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await this.ftp.downloadTo(localPath, remotePath);
|
|
101
|
+
}
|
|
102
|
+
async remove(remotePath) {
|
|
103
|
+
this.ensure();
|
|
104
|
+
if (this.sftp) {
|
|
105
|
+
await this.sftp.delete(remotePath);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await this.ftp.remove(remotePath);
|
|
109
|
+
}
|
|
110
|
+
async mkdir(remotePath, recursive) {
|
|
111
|
+
this.ensure();
|
|
112
|
+
if (this.sftp) {
|
|
113
|
+
await this.sftp.mkdir(remotePath, recursive);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await this.ftp.ensureDir(remotePath);
|
|
117
|
+
await this.ftp.cd("/");
|
|
118
|
+
}
|
|
119
|
+
async remoteSize(remotePath) {
|
|
120
|
+
this.ensure();
|
|
121
|
+
if (this.sftp) {
|
|
122
|
+
const s = await this.sftp.stat(remotePath);
|
|
123
|
+
return s.size;
|
|
124
|
+
}
|
|
125
|
+
return this.ftp.size(remotePath);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Herzstueck der Beweis-DNA: liest die Remote-Datei als Strom zurueck
|
|
129
|
+
* und berechnet ihren SHA-256 - ohne sie auf Platte zu speichern.
|
|
130
|
+
*/
|
|
131
|
+
async remoteSha256(remotePath) {
|
|
132
|
+
this.ensure();
|
|
133
|
+
const hash = createHash("sha256");
|
|
134
|
+
let bytes = 0;
|
|
135
|
+
const sink = new Writable({
|
|
136
|
+
write(chunk, _enc, cb) {
|
|
137
|
+
hash.update(chunk);
|
|
138
|
+
bytes += chunk.length;
|
|
139
|
+
cb();
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
if (this.sftp) {
|
|
143
|
+
await this.sftp.get(remotePath, sink);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
await this.ftp.downloadTo(sink, remotePath);
|
|
147
|
+
}
|
|
148
|
+
return { sha256: hash.digest("hex"), bytes };
|
|
149
|
+
}
|
|
150
|
+
async rename(fromPath, toPath) {
|
|
151
|
+
this.ensure();
|
|
152
|
+
if (this.sftp) {
|
|
153
|
+
await this.sftp.rename(fromPath, toPath);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await this.ftp.rename(fromPath, toPath);
|
|
157
|
+
}
|
|
158
|
+
async uploadDir(localDir, remoteDir) {
|
|
159
|
+
this.ensure();
|
|
160
|
+
if (this.sftp) {
|
|
161
|
+
await this.sftp.uploadDir(localDir, remoteDir);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
await this.ftp.uploadFromDir(localDir, remoteDir);
|
|
165
|
+
await this.ftp.cd("/");
|
|
166
|
+
}
|
|
167
|
+
async downloadDir(remoteDir, localDir) {
|
|
168
|
+
this.ensure();
|
|
169
|
+
if (this.sftp) {
|
|
170
|
+
await this.sftp.downloadDir(remoteDir, localDir);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await this.ftp.downloadToDir(localDir, remoteDir);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Beweis-Maschine: erst beweisen, dann behaupten.
|
|
3
|
+
* Jeder Transfer bekommt sein Urteil - PROVEN nur bei identischem SHA-256.
|
|
4
|
+
*/
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { createReadStream } from "node:fs";
|
|
7
|
+
export function localSha256(path) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const hash = createHash("sha256");
|
|
10
|
+
let bytes = 0;
|
|
11
|
+
const rs = createReadStream(path);
|
|
12
|
+
rs.on("data", (chunk) => {
|
|
13
|
+
hash.update(chunk);
|
|
14
|
+
bytes += chunk.length;
|
|
15
|
+
});
|
|
16
|
+
rs.on("end", () => resolve({ sha256: hash.digest("hex"), bytes }));
|
|
17
|
+
rs.on("error", reject);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function judge(local, remote, mode) {
|
|
21
|
+
if (mode === "none") {
|
|
22
|
+
return { verdict: "UNVERIFIED", detail: "Beweis abgewaehlt (proof=none) - Transfer ohne Urteil." };
|
|
23
|
+
}
|
|
24
|
+
if (mode === "size") {
|
|
25
|
+
const ok = local.bytes !== undefined && local.bytes === remote.bytes;
|
|
26
|
+
return {
|
|
27
|
+
verdict: ok ? "SIZE_MATCH" : "MISMATCH",
|
|
28
|
+
detail: ok
|
|
29
|
+
? `Groessen identisch (${local.bytes} Bytes) - schwacher Beweis, fuer harte Faelle proof=hash nutzen.`
|
|
30
|
+
: `Groessen weichen ab: lokal ${local.bytes} vs remote ${remote.bytes} Bytes.`,
|
|
31
|
+
local_bytes: local.bytes,
|
|
32
|
+
remote_bytes: remote.bytes,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const ok = !!local.sha256 && local.sha256 === remote.sha256;
|
|
36
|
+
return {
|
|
37
|
+
verdict: ok ? "PROVEN" : "MISMATCH",
|
|
38
|
+
detail: ok
|
|
39
|
+
? "SHA-256 identisch - Transfer bewiesen."
|
|
40
|
+
: "SHA-256 weicht ab - Transfer NICHT vertrauen, erneut uebertragen.",
|
|
41
|
+
local_sha256: local.sha256,
|
|
42
|
+
remote_sha256: remote.sha256,
|
|
43
|
+
local_bytes: local.bytes,
|
|
44
|
+
remote_bytes: remote.bytes,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { readdirSync, statSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { localSha256 } from "../services/proof.js";
|
|
5
|
+
function ok(output) {
|
|
6
|
+
return {
|
|
7
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
8
|
+
structuredContent: output,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function fail(err, hint) {
|
|
12
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13
|
+
const output = { error: msg, hint };
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
16
|
+
structuredContent: output,
|
|
17
|
+
isError: true,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Alle Dateien unter dir, als relative Pfade mit / als Trenner. */
|
|
21
|
+
function walkLocal(dir, base = dir) {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const name of readdirSync(dir)) {
|
|
24
|
+
const full = join(dir, name);
|
|
25
|
+
if (statSync(full).isDirectory())
|
|
26
|
+
out.push(...walkLocal(full, base));
|
|
27
|
+
else
|
|
28
|
+
out.push(relative(base, full).split("\\").join("/"));
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
export function registerDirTools(server, cm) {
|
|
33
|
+
server.registerTool("silicon_rename", {
|
|
34
|
+
title: "Umbenennen / Verschieben",
|
|
35
|
+
description: "Benennt eine Remote-Datei oder ein Verzeichnis um bzw. verschiebt es.",
|
|
36
|
+
inputSchema: {
|
|
37
|
+
fromPath: z.string().describe("Aktueller Remote-Pfad"),
|
|
38
|
+
toPath: z.string().describe("Neuer Remote-Pfad"),
|
|
39
|
+
},
|
|
40
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
41
|
+
}, async ({ fromPath, toPath }) => {
|
|
42
|
+
try {
|
|
43
|
+
await cm.rename(fromPath, toPath);
|
|
44
|
+
return ok({ status: "umbenannt", fromPath, toPath });
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return fail(e, "Quelle vorhanden? Ziel frei? Rechte pruefen.");
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
server.registerTool("silicon_upload_dir", {
|
|
51
|
+
title: "Verzeichnis hochladen (mit Beweis-Wanderung)",
|
|
52
|
+
description: "Laedt ein lokales Verzeichnis rekursiv hoch. Danach wird jede Datei einzeln verglichen: proof=hash prueft SHA-256 pro Datei (gruendlich, langsamer), proof=size vergleicht Groessen (schnell), none ueberspringt.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
localDir: z.string().describe("Lokales Quellverzeichnis"),
|
|
55
|
+
remoteDir: z.string().describe("Remote-Zielverzeichnis"),
|
|
56
|
+
proof: z.enum(["hash", "size", "none"]).default("size").describe("Beweisstufe pro Datei (Standard: size)"),
|
|
57
|
+
},
|
|
58
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
59
|
+
}, async ({ localDir, remoteDir, proof }) => {
|
|
60
|
+
try {
|
|
61
|
+
const t0 = Date.now();
|
|
62
|
+
const files = walkLocal(localDir);
|
|
63
|
+
await cm.uploadDir(localDir, remoteDir);
|
|
64
|
+
const mismatches = [];
|
|
65
|
+
let checked = 0;
|
|
66
|
+
if (proof !== "none") {
|
|
67
|
+
for (const rel of files) {
|
|
68
|
+
const lp = join(localDir, rel);
|
|
69
|
+
const rp = `${remoteDir.replace(/\/$/, "")}/${rel}`;
|
|
70
|
+
if (proof === "hash") {
|
|
71
|
+
const [l, r] = await Promise.all([localSha256(lp), cm.remoteSha256(rp)]);
|
|
72
|
+
checked++;
|
|
73
|
+
if (l.sha256 !== r.sha256)
|
|
74
|
+
mismatches.push({ file: rel, local_sha256: l.sha256, remote_sha256: r.sha256 });
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const lb = statSync(lp).size;
|
|
78
|
+
const rb = await cm.remoteSize(rp);
|
|
79
|
+
checked++;
|
|
80
|
+
if (lb !== rb)
|
|
81
|
+
mismatches.push({ file: rel, local_bytes: lb, remote_bytes: rb });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const verdict = proof === "none" ? "UNVERIFIED" : mismatches.length === 0 ? (proof === "hash" ? "PROVEN" : "SIZE_MATCH") : "MISMATCH";
|
|
86
|
+
return ok({
|
|
87
|
+
localDir,
|
|
88
|
+
remoteDir,
|
|
89
|
+
files_total: files.length,
|
|
90
|
+
files_checked: checked,
|
|
91
|
+
duration_ms: Date.now() - t0,
|
|
92
|
+
proof: { verdict, mismatches },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
return fail(e, "Lokales Verzeichnis vorhanden? Remote-Rechte? Bei grossen Baeumen proof=size nutzen.");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
server.registerTool("silicon_download_dir", {
|
|
100
|
+
title: "Verzeichnis herunterladen (mit Beweis-Wanderung)",
|
|
101
|
+
description: "Laedt ein Remote-Verzeichnis rekursiv herunter. Danach wird jede lokale Datei gegen die Remote-Quelle verglichen (hash/size/none, Standard size).",
|
|
102
|
+
inputSchema: {
|
|
103
|
+
remoteDir: z.string().describe("Remote-Quellverzeichnis"),
|
|
104
|
+
localDir: z.string().describe("Lokales Zielverzeichnis"),
|
|
105
|
+
proof: z.enum(["hash", "size", "none"]).default("size").describe("Beweisstufe pro Datei (Standard: size)"),
|
|
106
|
+
},
|
|
107
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
108
|
+
}, async ({ remoteDir, localDir, proof }) => {
|
|
109
|
+
try {
|
|
110
|
+
const t0 = Date.now();
|
|
111
|
+
await cm.downloadDir(remoteDir, localDir);
|
|
112
|
+
const files = walkLocal(localDir);
|
|
113
|
+
const mismatches = [];
|
|
114
|
+
let checked = 0;
|
|
115
|
+
if (proof !== "none") {
|
|
116
|
+
for (const rel of files) {
|
|
117
|
+
const lp = join(localDir, rel);
|
|
118
|
+
const rp = `${remoteDir.replace(/\/$/, "")}/${rel}`;
|
|
119
|
+
if (proof === "hash") {
|
|
120
|
+
const [l, r] = await Promise.all([localSha256(lp), cm.remoteSha256(rp)]);
|
|
121
|
+
checked++;
|
|
122
|
+
if (l.sha256 !== r.sha256)
|
|
123
|
+
mismatches.push({ file: rel, local_sha256: l.sha256, remote_sha256: r.sha256 });
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
const lb = statSync(lp).size;
|
|
127
|
+
const rb = await cm.remoteSize(rp);
|
|
128
|
+
checked++;
|
|
129
|
+
if (lb !== rb)
|
|
130
|
+
mismatches.push({ file: rel, local_bytes: lb, remote_bytes: rb });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const verdict = proof === "none" ? "UNVERIFIED" : mismatches.length === 0 ? (proof === "hash" ? "PROVEN" : "SIZE_MATCH") : "MISMATCH";
|
|
135
|
+
return ok({
|
|
136
|
+
remoteDir,
|
|
137
|
+
localDir,
|
|
138
|
+
files_total: files.length,
|
|
139
|
+
files_checked: checked,
|
|
140
|
+
duration_ms: Date.now() - t0,
|
|
141
|
+
proof: { verdict, mismatches },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
return fail(e, "Remote-Verzeichnis vorhanden? Lokale Schreibrechte? Bei grossen Baeumen proof=size nutzen.");
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|