@drop2run/mcp 0.1.1 → 0.3.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 +7 -3
- package/dist/index.js +102 -20
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ effect without a restart.
|
|
|
59
59
|
|
|
60
60
|
| Tool | What it does |
|
|
61
61
|
|---|---|
|
|
62
|
-
| `
|
|
62
|
+
| `publish_files` | Publishes files Claude wrote — a page, a markdown note, several files together |
|
|
63
63
|
| `publish_dir` | Publishes a folder, given its absolute path |
|
|
64
64
|
| `list_sites` | Lists the sites on the account |
|
|
65
65
|
|
|
@@ -71,8 +71,12 @@ new site is created.
|
|
|
71
71
|
**Publishing without a `site` creates a new one.** It never replaces your most recent site by default:
|
|
72
72
|
"put this online" is not "and overwrite what I published last time".
|
|
73
73
|
|
|
74
|
-
**A
|
|
75
|
-
|
|
74
|
+
**A site can be documents instead of a built site.** A publish needs an `index.html` at the top level,
|
|
75
|
+
or at least one `.md`, `.markdown` or `.pdf` file — those are served through the reader. So a single
|
|
76
|
+
note is a whole site, and Claude does not have to wrap it in HTML to publish it.
|
|
77
|
+
|
|
78
|
+
**`publish_files` is text.** A PDF or an image has to come off a disk with `publish_dir`, because these
|
|
79
|
+
files arrive as JSON strings.
|
|
76
80
|
|
|
77
81
|
**The URL comes back immediately; every edge has it in about a minute.** The reply also says whether a
|
|
78
82
|
new version was published, or every file already matched what the site serves.
|
package/dist/index.js
CHANGED
|
@@ -200,6 +200,56 @@ function isDocumentPath(path) {
|
|
|
200
200
|
const lower = path.toLowerCase();
|
|
201
201
|
return DOCUMENT_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
202
202
|
}
|
|
203
|
+
const VIEWABLE_EXTENSIONS = [
|
|
204
|
+
// Documents — these also count at any depth, via DOCUMENT_EXTENSIONS.
|
|
205
|
+
".md",
|
|
206
|
+
".markdown",
|
|
207
|
+
".pdf",
|
|
208
|
+
// Prose the reader shows preformatted.
|
|
209
|
+
".txt",
|
|
210
|
+
".log",
|
|
211
|
+
".csv",
|
|
212
|
+
// Source the reader shows with highlighting.
|
|
213
|
+
".json",
|
|
214
|
+
".yaml",
|
|
215
|
+
".yml",
|
|
216
|
+
".toml",
|
|
217
|
+
".css",
|
|
218
|
+
".js",
|
|
219
|
+
".ts",
|
|
220
|
+
".tsx",
|
|
221
|
+
".jsx",
|
|
222
|
+
".sh",
|
|
223
|
+
".xml",
|
|
224
|
+
// Pictures the reader shows with an img element.
|
|
225
|
+
".png",
|
|
226
|
+
".jpg",
|
|
227
|
+
".jpeg",
|
|
228
|
+
".gif",
|
|
229
|
+
".webp",
|
|
230
|
+
".avif",
|
|
231
|
+
".svg",
|
|
232
|
+
".ico",
|
|
233
|
+
// See the note above: a floor for a caller that did not rename it.
|
|
234
|
+
".html",
|
|
235
|
+
".htm"
|
|
236
|
+
];
|
|
237
|
+
function isViewablePath(path) {
|
|
238
|
+
const lower = path.toLowerCase();
|
|
239
|
+
return VIEWABLE_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
240
|
+
}
|
|
241
|
+
function canPublish(paths) {
|
|
242
|
+
if (paths.some((path) => path === REQUIRED_INDEX)) return true;
|
|
243
|
+
if (paths.some(isDocumentPath)) return true;
|
|
244
|
+
return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
|
|
245
|
+
}
|
|
246
|
+
function renameOfLoneHtmlPage(paths) {
|
|
247
|
+
if (paths.length !== 1) return null;
|
|
248
|
+
const [only] = paths;
|
|
249
|
+
if (only === void 0 || only === REQUIRED_INDEX) return null;
|
|
250
|
+
const lower = only.toLowerCase();
|
|
251
|
+
return lower.endsWith(".html") || lower.endsWith(".htm") ? only : null;
|
|
252
|
+
}
|
|
203
253
|
function formatBytes(bytes) {
|
|
204
254
|
if (bytes < 1024) return `${bytes} B`;
|
|
205
255
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -213,11 +263,10 @@ function checkLimits(files, limits) {
|
|
|
213
263
|
"That drop contained no files to deploy. Folders like .git and node_modules are skipped."
|
|
214
264
|
);
|
|
215
265
|
}
|
|
216
|
-
|
|
217
|
-
if (!hasIndex && !files.some((file) => isDocumentPath(file.path))) {
|
|
266
|
+
if (!canPublish(files.map((file) => file.path))) {
|
|
218
267
|
throw new DeployError(
|
|
219
268
|
ClientErrorCode.MissingIndex,
|
|
220
|
-
`A site needs an ${REQUIRED_INDEX} at its top level, or at least one .md or .pdf file to publish as a documents site. Drop the folder that contains it, not the folder above.`
|
|
269
|
+
`A site needs an ${REQUIRED_INDEX} at its top level, or at least one .md or .pdf file to publish as a documents site — or be a single file that can be displayed on its own, such as a note, a PDF, a page or an image. Drop the folder that contains it, not the folder above.`
|
|
221
270
|
);
|
|
222
271
|
}
|
|
223
272
|
if (!limits) return;
|
|
@@ -228,9 +277,7 @@ function checkLimits(files, limits) {
|
|
|
228
277
|
{ limit: limits.maxFiles, actual: files.length }
|
|
229
278
|
);
|
|
230
279
|
}
|
|
231
|
-
const oversized = files.find(
|
|
232
|
-
(file) => file.bytes.length > limits.maxFileBytes
|
|
233
|
-
);
|
|
280
|
+
const oversized = files.find((file) => file.bytes.length > limits.maxFileBytes);
|
|
234
281
|
if (oversized) {
|
|
235
282
|
throw new DeployError(
|
|
236
283
|
ClientErrorCode.FileTooLarge,
|
|
@@ -459,8 +506,10 @@ function asDeployError(error) {
|
|
|
459
506
|
}
|
|
460
507
|
async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
461
508
|
try {
|
|
462
|
-
const
|
|
463
|
-
onProgress({ type: "collecting", files:
|
|
509
|
+
const collected = await source(signal);
|
|
510
|
+
onProgress({ type: "collecting", files: collected.length });
|
|
511
|
+
const rename = renameOfLoneHtmlPage(collected.map((file) => file.path));
|
|
512
|
+
const files = rename === null ? collected : collected.map((file) => file.path === rename ? { ...file, path: "index.html" } : file);
|
|
464
513
|
const hashed = await hashAll(
|
|
465
514
|
files,
|
|
466
515
|
(done, total) => onProgress({ type: "hashing", done, total }),
|
|
@@ -595,11 +644,32 @@ async function publish(credentials, source, site) {
|
|
|
595
644
|
function publishDirectory(credentials, directory, site) {
|
|
596
645
|
return publish(credentials, directorySource(directory), site);
|
|
597
646
|
}
|
|
598
|
-
function
|
|
599
|
-
const
|
|
600
|
-
|
|
647
|
+
function cleanPath(path) {
|
|
648
|
+
const cleaned = path.trim().replaceAll("\\", "/").replace(/^\.?\//, "");
|
|
649
|
+
if (cleaned === "" || cleaned.endsWith("/")) {
|
|
650
|
+
throw new Error(`"${path}" does not name a file.`);
|
|
651
|
+
}
|
|
652
|
+
if (cleaned.split("/").includes("..") || /^[a-zA-Z]:/.test(cleaned)) {
|
|
653
|
+
throw new Error(
|
|
654
|
+
`"${path}" is not a path inside the site. Give a path relative to the site's root, such as index.html or docs/guide.md.`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return cleaned;
|
|
601
658
|
}
|
|
602
|
-
|
|
659
|
+
async function publishFiles(credentials, files, site) {
|
|
660
|
+
const encoder = new TextEncoder();
|
|
661
|
+
const collected = files.map((file) => ({
|
|
662
|
+
path: cleanPath(file.path),
|
|
663
|
+
bytes: encoder.encode(file.content)
|
|
664
|
+
}));
|
|
665
|
+
const seen = /* @__PURE__ */ new Set();
|
|
666
|
+
for (const file of collected) {
|
|
667
|
+
if (seen.has(file.path)) throw new Error(`Two files were given the same path: ${file.path}`);
|
|
668
|
+
seen.add(file.path);
|
|
669
|
+
}
|
|
670
|
+
return publish(credentials, async () => collected, site);
|
|
671
|
+
}
|
|
672
|
+
const SERVER_VERSION = "0.3.0";
|
|
603
673
|
function say(text, isError = false) {
|
|
604
674
|
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
|
|
605
675
|
}
|
|
@@ -629,9 +699,14 @@ function createServer() {
|
|
|
629
699
|
instructions: [
|
|
630
700
|
"Publishes static sites to Drop2Run.",
|
|
631
701
|
"",
|
|
632
|
-
"Use
|
|
633
|
-
"
|
|
634
|
-
"publish over an existing one. list_sites
|
|
702
|
+
"Use publish_files for anything you wrote here — a page, a markdown note, several files",
|
|
703
|
+
"together — and publish_dir for a folder that already exists on disk. Leave `site` unset to",
|
|
704
|
+
"publish to a brand-new site; pass a subdomain to publish over an existing one. list_sites",
|
|
705
|
+
"shows what already exists.",
|
|
706
|
+
"",
|
|
707
|
+
"A site needs an index.html at its top level, or at least one .md, .markdown or .pdf file,",
|
|
708
|
+
"which publishes as a documents site and is read through a viewer. So one page goes at",
|
|
709
|
+
"index.html, and a single note is a whole publish that needs no wrapping in HTML.",
|
|
635
710
|
"",
|
|
636
711
|
"Publishing over an existing site replaces what it serves, so ask before doing that to a",
|
|
637
712
|
"site the person did not name."
|
|
@@ -639,16 +714,23 @@ function createServer() {
|
|
|
639
714
|
}
|
|
640
715
|
);
|
|
641
716
|
server.registerTool(
|
|
642
|
-
"
|
|
717
|
+
"publish_files",
|
|
643
718
|
{
|
|
644
|
-
title: "Publish
|
|
645
|
-
description: "Publishes one HTML
|
|
719
|
+
title: "Publish files you wrote",
|
|
720
|
+
description: "Publishes one or more files written here — markdown, HTML, CSS, JSON — as a site, and returns its URL. A single page goes at index.html; a single .md, .markdown or .pdf file is a site on its own, served through the documents viewer. Text only: a PDF or an image has to be published from disk with publish_dir.",
|
|
646
721
|
inputSchema: {
|
|
647
|
-
|
|
722
|
+
files: z.array(
|
|
723
|
+
z.object({
|
|
724
|
+
path: z.string().min(1).describe("Path inside the site, such as index.html or docs/guide.md."),
|
|
725
|
+
content: z.string().describe("The file's contents.")
|
|
726
|
+
})
|
|
727
|
+
).min(1).describe("The files to publish."),
|
|
648
728
|
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site.")
|
|
649
729
|
}
|
|
650
730
|
},
|
|
651
|
-
({
|
|
731
|
+
({ files, site }) => withCredentials(
|
|
732
|
+
async (credentials) => describe(await publishFiles(credentials, files, site))
|
|
733
|
+
)
|
|
652
734
|
);
|
|
653
735
|
server.registerTool(
|
|
654
736
|
"publish_dir",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drop2run/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server that publishes a folder or an HTML page to Drop2Run and returns a live URL.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
"access": "public"
|
|
19
19
|
},
|
|
20
20
|
"homepage": "https://dropto.run",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Drop-to-run/drop2run-cli.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Drop-to-run/drop2run-cli/issues"
|
|
27
|
+
},
|
|
21
28
|
"keywords": [
|
|
22
29
|
"mcp",
|
|
23
30
|
"ai",
|