@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.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/index.js +102 -20
  3. 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
- | `publish_html` | Publishes one HTML document as a site. It becomes `index.html` |
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 folder can be documents instead of a built site.** `publish_dir` wants an `index.html` at the top
75
- level, or at least one `.md`, `.markdown` or `.pdf` file — those are served through the reader.
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
- const hasIndex = files.some((file) => file.path === REQUIRED_INDEX);
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 files = await source(signal);
463
- onProgress({ type: "collecting", files: files.length });
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 publishHtml(credentials, html, site) {
599
- const bytes = new TextEncoder().encode(html);
600
- return publish(credentials, async () => [{ path: "index.html", bytes }], site);
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
- const SERVER_VERSION = "0.1.1";
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 publish_html for a single page you have written, and publish_dir for a folder that is",
633
- "already built. Leave `site` unset to publish to a brand-new site; pass a subdomain to",
634
- "publish over an existing one. list_sites shows what already exists.",
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
- "publish_html",
717
+ "publish_files",
643
718
  {
644
- title: "Publish a page",
645
- description: "Publishes one HTML page as a whole site and returns its URL. The page becomes index.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
- html: z.string().min(1).describe("The complete HTML document to publish."),
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
- ({ html, site }) => withCredentials(async (credentials) => describe(await publishHtml(credentials, html, site)))
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.1.1",
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",