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 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
+ }