drops-mcp 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/install.mjs ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * drops-install — wire the drops MCP server (and `drop` CLI) into your agents.
4
+ *
5
+ * Open-source, self-hosted counterpart to a one-command installer: detects the agent
6
+ * CLIs you have and registers the `drops` MCP server, symlinks `drop` onto your PATH,
7
+ * and prints copy-paste config for GUI MCP clients.
8
+ *
9
+ * node skill/install.mjs # register into detected agents
10
+ * node skill/install.mjs --print # just print config, change nothing
11
+ *
12
+ * Run `drop setup` (or `drop init` + `drop setup`) afterwards to provision the Blob token.
13
+ */
14
+
15
+ import { spawnSync } from "node:child_process";
16
+ import { existsSync, symlinkSync, mkdirSync, rmSync } from "node:fs";
17
+ import { homedir } from "node:os";
18
+ import { join, dirname } from "node:path";
19
+ import { fileURLToPath } from "node:url";
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const MCP = join(__dirname, "mcp.mjs");
23
+ const DROP = join(__dirname, "drop.mjs");
24
+ const PRINT = process.argv.includes("--print");
25
+
26
+ // When installed from npm, wire agents to the published runner (survives upgrades);
27
+ // from a local clone, point at the local mcp.mjs.
28
+ const PUBLISHED = __dirname.includes(`${require_sep()}node_modules${require_sep()}`);
29
+ function require_sep() { return process.platform === "win32" ? "\\" : "/"; }
30
+ const MCP_CMD = PUBLISHED ? ["npx", "-y", "drops-mcp"] : [process.execPath, MCP];
31
+
32
+ const ok = (m) => console.log(`\x1b[32m✓\x1b[0m ${m}`);
33
+ const dim = (m) => console.log(`\x1b[2m${m}\x1b[0m`);
34
+ const has = (cmd) => spawnSync(process.platform === "win32" ? "where" : "which", [cmd], { stdio: "ignore", shell: process.platform === "win32" }).status === 0;
35
+
36
+ // CLI agents that support `<cli> mcp add <name> -- <command>`.
37
+ const CLI_AGENTS = ["claude", "codex", "opencode", "amp"];
38
+
39
+ console.log("drops-install — wiring the drops MCP into your agents\n");
40
+
41
+ let wired = 0;
42
+ for (const cli of CLI_AGENTS) {
43
+ if (!has(cli)) continue;
44
+ const args = ["mcp", "add", "drops", "--", ...MCP_CMD];
45
+ if (PRINT) { dim(`${cli} ${args.join(" ")}`); wired++; continue; }
46
+ const r = spawnSync(cli, args, { stdio: "inherit", shell: process.platform === "win32" });
47
+ if (r.status === 0) { ok(`registered drops MCP in ${cli}`); wired++; }
48
+ else dim(`skipped ${cli} (add failed — may already exist)`);
49
+ }
50
+ if (!wired) dim("no CLI agents detected (claude / codex / opencode / amp)");
51
+
52
+ // Symlink `drop` onto PATH for shell/agent use.
53
+ const bin = join(homedir(), ".local", "bin");
54
+ const link = join(bin, "drop");
55
+ if (PRINT) {
56
+ dim(`ln -s ${DROP} ${link}`);
57
+ } else {
58
+ try {
59
+ mkdirSync(bin, { recursive: true });
60
+ if (existsSync(link)) rmSync(link);
61
+ symlinkSync(DROP, link);
62
+ ok(`linked 'drop' → ${link} (ensure ${bin} is on your PATH)`);
63
+ } catch (e) { dim(`could not symlink drop: ${e.message}`); }
64
+ }
65
+
66
+ // Copy-paste config for GUI MCP clients (Cursor / Claude Desktop / Windsurf / Zed).
67
+ console.log("\nFor GUI MCP clients, add this to your MCP config:\n");
68
+ console.log(JSON.stringify({ mcpServers: { drops: { command: MCP_CMD[0], args: MCP_CMD.slice(1) } } }, null, 2));
69
+
70
+ console.log("\nNext: drop init --domain ... (BYO domain/Blob) then drop setup --token vercel_blob_rw_...");
package/mcp.mjs ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * drops MCP server — the MCP-native publish primitive for HTML/artifacts your agents make.
4
+ *
5
+ * Open-source, self-hosted counterpart to Stacktree's MCP: an agent calls `publish_html`
6
+ * and gets back a branded, password-protected, zero-knowledge link on YOUR own domain.
7
+ * Every tool shells out to the battle-tested `drop` CLI (drop.mjs --json) so the MCP and
8
+ * CLI share one pipeline. Nothing is hosted by a third party — it's your Vercel Blob.
9
+ *
10
+ * Wire it into an agent (stdio):
11
+ * claude mcp add drops -- npx -y --prefix <repo>/skill drops-mcp # once published
12
+ * # or, from a clone:
13
+ * claude mcp add drops -- node <repo>/skill/mcp.mjs
14
+ *
15
+ * Requires `drop setup` to have run (BLOB_READ_WRITE_TOKEN in ~/.drop/.env).
16
+ */
17
+
18
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import { z } from "zod";
21
+ import { spawn } from "node:child_process";
22
+ import { writeFile, mkdtemp, rm } from "node:fs/promises";
23
+ import { tmpdir } from "node:os";
24
+ import { join, dirname } from "node:path";
25
+ import { fileURLToPath } from "node:url";
26
+
27
+ const __dirname = dirname(fileURLToPath(import.meta.url));
28
+ const DROP = join(__dirname, "drop.mjs");
29
+
30
+ // Run the drop CLI with --json and return the parsed result.
31
+ function runDrop(args) {
32
+ return new Promise((resolve, reject) => {
33
+ const p = spawn(process.execPath, [DROP, ...args, "--json"], { stdio: ["ignore", "pipe", "pipe"] });
34
+ let out = "", err = "";
35
+ p.stdout.on("data", (d) => (out += d));
36
+ p.stderr.on("data", (d) => (err += d));
37
+ p.on("error", reject);
38
+ p.on("close", (code) => {
39
+ if (code !== 0) return reject(new Error(err.trim() || `drop exited ${code}`));
40
+ const line = out.trim().split("\n").filter(Boolean).pop() || "{}";
41
+ try { resolve(JSON.parse(line)); } catch { resolve({ raw: out.trim() }); }
42
+ });
43
+ });
44
+ }
45
+
46
+ async function withTempFile(name, content, fn) {
47
+ const dir = await mkdtemp(join(tmpdir(), "drops-mcp-"));
48
+ const path = join(dir, name);
49
+ await writeFile(path, content);
50
+ try { return await fn(path); }
51
+ finally { await rm(dir, { recursive: true, force: true }); }
52
+ }
53
+
54
+ const textResult = (obj) => ({ content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] });
55
+ const errResult = (e) => ({ isError: true, content: [{ type: "text", text: `Error: ${e.message || e}` }] });
56
+
57
+ const server = new McpServer({ name: "drops", version: "1.0.0" });
58
+
59
+ server.tool(
60
+ "publish_html",
61
+ "Publish raw HTML as a branded, password-protected (zero-knowledge AES-256) link on your own domain. Returns the URL and (if locked) the password. Locked by default.",
62
+ {
63
+ html: z.string().describe("The full HTML document to publish."),
64
+ slug: z.string().optional().describe("Force the URL slug (e.g. 'q3-report'). Otherwise auto-generated."),
65
+ password: z.string().optional().describe("Set a specific password. Otherwise an unguessable one is generated when locked."),
66
+ lock: z.boolean().optional().describe("Encrypt + password-protect (default true). Set false for a public branded page."),
67
+ expire: z.string().optional().describe("Auto-expire after this long: '7d', '24h', '2w', or a date. Enforce deletion with the host's `drop gc` cron."),
68
+ },
69
+ async ({ html, slug, password, lock, expire }) => {
70
+ try {
71
+ const args = [];
72
+ if (slug) args.push("-s", slug);
73
+ if (lock === false) args.push("--no-lock");
74
+ else if (password) args.push("-p", password);
75
+ if (expire) args.push("--expire", expire);
76
+ const res = await withTempFile((slug || "page") + ".html", html, (p) => runDrop([p, ...args]));
77
+ return textResult(res);
78
+ } catch (e) { return errResult(e); }
79
+ }
80
+ );
81
+
82
+ server.tool(
83
+ "publish_file",
84
+ "Publish a file from a local path. Non-HTML files get an unguessable URL; pass page=true to wrap any file in a branded download page (optionally password-protected).",
85
+ {
86
+ path: z.string().describe("Absolute path to the local file to publish."),
87
+ slug: z.string().optional(),
88
+ page: z.boolean().optional().describe("Wrap the file in a branded download page."),
89
+ password: z.string().optional().describe("Password-protect the download page (requires page=true)."),
90
+ },
91
+ async ({ path, slug, page, password }) => {
92
+ try {
93
+ const args = [path];
94
+ if (slug) args.push("-s", slug);
95
+ if (page) args.push("--page");
96
+ if (password) args.push("-p", password);
97
+ return textResult(await runDrop(args));
98
+ } catch (e) { return errResult(e); }
99
+ }
100
+ );
101
+
102
+ server.tool(
103
+ "update_site",
104
+ "Replace the content of an existing drop in place — same URL/slug, new HTML. (Set a password to re-lock; zero-knowledge means content can't be re-keyed without re-supplying it.)",
105
+ {
106
+ slug: z.string().describe("The slug to overwrite."),
107
+ html: z.string().describe("The new HTML document."),
108
+ password: z.string().optional(),
109
+ lock: z.boolean().optional(),
110
+ expire: z.string().optional().describe("Auto-expire: '7d', '24h', '2w', or a date."),
111
+ },
112
+ async ({ slug, html, password, lock, expire }) => {
113
+ try {
114
+ const args = ["-s", slug];
115
+ if (lock === false) args.push("--no-lock");
116
+ else if (password) args.push("-p", password);
117
+ if (expire) args.push("--expire", expire);
118
+ const res = await withTempFile(slug + ".html", html, (p) => runDrop([p, ...args]));
119
+ return textResult(res);
120
+ } catch (e) { return errResult(e); }
121
+ }
122
+ );
123
+
124
+ server.tool(
125
+ "list_sites",
126
+ "List the drops currently live in your store (slug, URL, size, password if known locally, upload date).",
127
+ {},
128
+ async () => {
129
+ try { return textResult(await runDrop(["list"])); }
130
+ catch (e) { return errResult(e); }
131
+ }
132
+ );
133
+
134
+ server.tool(
135
+ "delete_site",
136
+ "Delete a drop (and any sibling file) by slug. Burns the artifact immediately.",
137
+ { slug: z.string().describe("The slug to delete.") },
138
+ async ({ slug }) => {
139
+ try { return textResult(await runDrop(["rm", slug])); }
140
+ catch (e) { return errResult(e); }
141
+ }
142
+ );
143
+
144
+ const transport = new StdioServerTransport();
145
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "drops-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Open-source artifact sharing — publish HTML/Markdown/files as branded, password-protected, zero-knowledge links on your own domain, from any AI agent. CLI + MCP server. The open-source, self-hosted Stacktree alternative.",
5
+ "type": "module",
6
+ "bin": {
7
+ "drop": "./drop.mjs",
8
+ "drops-mcp": "./mcp.mjs",
9
+ "drops-install": "./install.mjs"
10
+ },
11
+ "files": [
12
+ "drop.mjs",
13
+ "mcp.mjs",
14
+ "install.mjs",
15
+ "brand/",
16
+ "SKILL.md",
17
+ "SETUP.md"
18
+ ],
19
+ "keywords": [
20
+ "mcp", "ai-agents", "claude", "claude-code", "artifacts", "file-sharing",
21
+ "zero-knowledge", "cli", "self-hosted", "staticrypt", "vercel-blob",
22
+ "password-protection", "stacktree-alternative", "publish-html"
23
+ ],
24
+ "repository": { "type": "git", "url": "https://github.com/maxtechera/drops-share" },
25
+ "homepage": "https://drops.maxtechera.dev",
26
+ "bugs": "https://github.com/maxtechera/drops-share/issues",
27
+ "author": "Max Techera (https://maxtechera.dev)",
28
+ "license": "MIT",
29
+ "engines": { "node": ">=18" },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.0.0",
32
+ "@vercel/blob": "^0.27.0",
33
+ "adm-zip": "^0.5.17",
34
+ "marked": "^18.0.5",
35
+ "zod": "^3.23.0"
36
+ }
37
+ }