@drop2run/mcp 0.5.1 → 0.6.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 +6 -4
  2. package/dist/index.js +514 -52
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # @drop2run/mcp
2
2
 
3
- An MCP server that lets Claude — or any MCP client — publish a static site to
4
- [Drop2Run](https://dropto.run). Give it a folder or an HTML document; it returns a live HTTPS URL.
3
+ An MCP server that lets Claude — or any MCP client — publish what it made to
4
+ [Drop2Run](https://dropto.run): an HTML page, a markdown note, a folder of documents or a built site. It
5
+ returns a live HTTPS URL.
5
6
 
6
7
  Full documentation at [dropto.run/docs/mcp](https://dropto.run/docs/mcp).
7
8
 
@@ -89,8 +90,9 @@ it as `subdomain`; say nothing and the name is generated, which is the right def
89
90
  nobody is going to link to.
90
91
 
91
92
  **A site can be documents instead of a built site.** A publish needs an `index.html` at the top level,
92
- or at least one `.md`, `.markdown` or `.pdf` file — those are served through the reader. So a single
93
- note is a whole site, and Claude does not have to wrap it in HTML to publish it.
93
+ or at least one document — `.md`, `.markdown`, `.pdf`, `.docx`, `.xlsx` or `.epub` — and those are
94
+ served through the reader. So a single note is a whole site, and Claude does not have to wrap it in HTML
95
+ to publish it.
94
96
 
95
97
  **`publish_files` is text.** A PDF or an image has to come off a disk with `publish_dir`, because these
96
98
  files arrive as JSON strings.
package/dist/index.js CHANGED
@@ -374,6 +374,485 @@ async function problemToError(response) {
374
374
  const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
375
375
  return new DeployError(code, message, problem);
376
376
  }
377
+ const FILE_TYPES = [
378
+ // Documents. The three extensions that make a folder a documents site.
379
+ {
380
+ ext: ".md",
381
+ mime: "text/markdown; charset=utf-8",
382
+ kind: "markdown",
383
+ document: true,
384
+ reader: true,
385
+ editable: true
386
+ },
387
+ {
388
+ ext: ".markdown",
389
+ mime: "text/markdown; charset=utf-8",
390
+ kind: "markdown",
391
+ document: true,
392
+ reader: true,
393
+ editable: true
394
+ },
395
+ {
396
+ ext: ".pdf",
397
+ mime: "application/pdf",
398
+ kind: "pdf",
399
+ document: true,
400
+ reader: true,
401
+ editable: false
402
+ },
403
+ // Office documents and ebooks. Each has its own kind because each needs its own renderer, loaded
404
+ // only when one is opened — `.docx` through mammoth, `.xlsx` through read-excel-file, `.epub`
405
+ // through foliate-js.
406
+ {
407
+ ext: ".docx",
408
+ mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
409
+ kind: "docx",
410
+ document: true,
411
+ reader: true,
412
+ editable: false
413
+ },
414
+ {
415
+ ext: ".xlsx",
416
+ mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
417
+ kind: "xlsx",
418
+ document: true,
419
+ reader: true,
420
+ editable: false
421
+ },
422
+ {
423
+ ext: ".epub",
424
+ mime: "application/epub+zip",
425
+ kind: "epub",
426
+ document: true,
427
+ reader: true,
428
+ editable: false
429
+ },
430
+ // Prose the reader shows preformatted.
431
+ {
432
+ ext: ".txt",
433
+ mime: "text/plain; charset=utf-8",
434
+ kind: "text",
435
+ document: false,
436
+ reader: true,
437
+ editable: true
438
+ },
439
+ {
440
+ ext: ".log",
441
+ mime: "text/plain; charset=utf-8",
442
+ kind: "text",
443
+ document: false,
444
+ reader: true,
445
+ editable: false
446
+ },
447
+ {
448
+ ext: ".csv",
449
+ mime: "text/csv; charset=utf-8",
450
+ kind: "text",
451
+ document: false,
452
+ reader: true,
453
+ editable: true
454
+ },
455
+ // Structured text. Read through the reader inside a folder of notes, which is why these carry
456
+ // `reader: true` while the source formats below do not.
457
+ {
458
+ ext: ".json",
459
+ mime: "application/json",
460
+ kind: "code",
461
+ document: false,
462
+ reader: true,
463
+ editable: true
464
+ },
465
+ {
466
+ ext: ".yaml",
467
+ mime: "text/yaml; charset=utf-8",
468
+ kind: "code",
469
+ document: false,
470
+ reader: true,
471
+ editable: true
472
+ },
473
+ {
474
+ ext: ".yml",
475
+ mime: "text/yaml; charset=utf-8",
476
+ kind: "code",
477
+ document: false,
478
+ reader: true,
479
+ editable: true
480
+ },
481
+ {
482
+ // No registered media type, so it takes text/plain rather than an invented one. The point is
483
+ // only that a browser shows it instead of downloading it.
484
+ ext: ".toml",
485
+ mime: "text/plain; charset=utf-8",
486
+ kind: "code",
487
+ document: false,
488
+ reader: true,
489
+ editable: true
490
+ },
491
+ // Source the viewer highlights. A link straight to one of these hands over the file.
492
+ {
493
+ ext: ".css",
494
+ mime: "text/css; charset=utf-8",
495
+ kind: "code",
496
+ document: false,
497
+ reader: false,
498
+ editable: true
499
+ },
500
+ {
501
+ ext: ".js",
502
+ mime: "text/javascript; charset=utf-8",
503
+ kind: "code",
504
+ document: false,
505
+ reader: false,
506
+ editable: true
507
+ },
508
+ {
509
+ ext: ".ts",
510
+ mime: null,
511
+ kind: "code",
512
+ document: false,
513
+ reader: false,
514
+ editable: false
515
+ },
516
+ {
517
+ ext: ".tsx",
518
+ mime: null,
519
+ kind: "code",
520
+ document: false,
521
+ reader: false,
522
+ editable: false
523
+ },
524
+ {
525
+ ext: ".jsx",
526
+ mime: null,
527
+ kind: "code",
528
+ document: false,
529
+ reader: false,
530
+ editable: false
531
+ },
532
+ {
533
+ ext: ".sh",
534
+ mime: null,
535
+ kind: "code",
536
+ document: false,
537
+ reader: false,
538
+ editable: false
539
+ },
540
+ // The languages an assistant writes most, beyond the web's own. Same row as `.ts`: shown as source
541
+ // when somebody opens one in the reader or publishes it on its own, handed over as a file when a
542
+ // link names it directly, and no media type — adding one changes what a browser does with a
543
+ // published file, which I10 makes a decision of its own rather than a side effect of this list.
544
+ {
545
+ ext: ".py",
546
+ mime: null,
547
+ kind: "code",
548
+ document: false,
549
+ reader: false,
550
+ editable: false
551
+ },
552
+ {
553
+ ext: ".sql",
554
+ mime: null,
555
+ kind: "code",
556
+ document: false,
557
+ reader: false,
558
+ editable: false
559
+ },
560
+ {
561
+ ext: ".go",
562
+ mime: null,
563
+ kind: "code",
564
+ document: false,
565
+ reader: false,
566
+ editable: false
567
+ },
568
+ {
569
+ ext: ".rs",
570
+ mime: null,
571
+ kind: "code",
572
+ document: false,
573
+ reader: false,
574
+ editable: false
575
+ },
576
+ {
577
+ ext: ".java",
578
+ mime: null,
579
+ kind: "code",
580
+ document: false,
581
+ reader: false,
582
+ editable: false
583
+ },
584
+ {
585
+ ext: ".kt",
586
+ mime: null,
587
+ kind: "code",
588
+ document: false,
589
+ reader: false,
590
+ editable: false
591
+ },
592
+ {
593
+ ext: ".swift",
594
+ mime: null,
595
+ kind: "code",
596
+ document: false,
597
+ reader: false,
598
+ editable: false
599
+ },
600
+ {
601
+ ext: ".rb",
602
+ mime: null,
603
+ kind: "code",
604
+ document: false,
605
+ reader: false,
606
+ editable: false
607
+ },
608
+ {
609
+ ext: ".php",
610
+ mime: null,
611
+ kind: "code",
612
+ document: false,
613
+ reader: false,
614
+ editable: false
615
+ },
616
+ {
617
+ ext: ".c",
618
+ mime: null,
619
+ kind: "code",
620
+ document: false,
621
+ reader: false,
622
+ editable: false
623
+ },
624
+ {
625
+ ext: ".h",
626
+ mime: null,
627
+ kind: "code",
628
+ document: false,
629
+ reader: false,
630
+ editable: false
631
+ },
632
+ {
633
+ ext: ".cpp",
634
+ mime: null,
635
+ kind: "code",
636
+ document: false,
637
+ reader: false,
638
+ editable: false
639
+ },
640
+ {
641
+ ext: ".cs",
642
+ mime: null,
643
+ kind: "code",
644
+ document: false,
645
+ reader: false,
646
+ editable: false
647
+ },
648
+ {
649
+ ext: ".r",
650
+ mime: null,
651
+ kind: "code",
652
+ document: false,
653
+ reader: false,
654
+ editable: false
655
+ },
656
+ {
657
+ ext: ".xml",
658
+ mime: "application/xml",
659
+ kind: "code",
660
+ document: false,
661
+ reader: false,
662
+ editable: true
663
+ },
664
+ // Pictures, shown with an img element.
665
+ {
666
+ ext: ".png",
667
+ mime: "image/png",
668
+ kind: "image",
669
+ document: false,
670
+ reader: false,
671
+ editable: false
672
+ },
673
+ {
674
+ ext: ".jpg",
675
+ mime: "image/jpeg",
676
+ kind: "image",
677
+ document: false,
678
+ reader: false,
679
+ editable: false
680
+ },
681
+ {
682
+ ext: ".jpeg",
683
+ mime: "image/jpeg",
684
+ kind: "image",
685
+ document: false,
686
+ reader: false,
687
+ editable: false
688
+ },
689
+ {
690
+ ext: ".gif",
691
+ mime: "image/gif",
692
+ kind: "image",
693
+ document: false,
694
+ reader: false,
695
+ editable: false
696
+ },
697
+ {
698
+ ext: ".webp",
699
+ mime: "image/webp",
700
+ kind: "image",
701
+ document: false,
702
+ reader: false,
703
+ editable: false
704
+ },
705
+ {
706
+ ext: ".avif",
707
+ mime: "image/avif",
708
+ kind: "image",
709
+ document: false,
710
+ reader: false,
711
+ editable: false
712
+ },
713
+ {
714
+ // Text, hand-edited in practice, and script-carrying. Editable anyway: the Worker derives the
715
+ // type at serve time (I10) and already serves whatever was uploaded, so withholding the edit
716
+ // would withhold nothing else.
717
+ ext: ".svg",
718
+ mime: "image/svg+xml",
719
+ kind: "image",
720
+ document: false,
721
+ reader: false,
722
+ editable: true
723
+ },
724
+ {
725
+ ext: ".ico",
726
+ mime: "image/x-icon",
727
+ kind: "image",
728
+ document: false,
729
+ reader: false,
730
+ editable: false
731
+ },
732
+ // A customer's own pages. Never rendered inside the reader.
733
+ {
734
+ ext: ".html",
735
+ mime: "text/html; charset=utf-8",
736
+ kind: "html",
737
+ document: false,
738
+ reader: false,
739
+ editable: true
740
+ },
741
+ {
742
+ // ⚠️ No media type, and that is the state of the code rather than a decision: `TYPES` in
743
+ // `headers.ts` has never held `htm`, so a published `.htm` is served as octet-stream and
744
+ // downloads instead of rendering. Recorded here rather than fixed, because this table was
745
+ // introduced as a refactor and a refactor that changes what a URL answers is not one.
746
+ ext: ".htm",
747
+ mime: null,
748
+ kind: "html",
749
+ document: false,
750
+ reader: false,
751
+ editable: true
752
+ },
753
+ // Everything below renders as nothing — the viewer offers a download — but still needs a media
754
+ // type, because these are the files a static site is built out of.
755
+ {
756
+ ext: ".mjs",
757
+ mime: "text/javascript; charset=utf-8",
758
+ kind: "binary",
759
+ document: false,
760
+ reader: false,
761
+ editable: true
762
+ },
763
+ {
764
+ ext: ".cjs",
765
+ mime: null,
766
+ kind: "binary",
767
+ document: false,
768
+ reader: false,
769
+ editable: true
770
+ },
771
+ {
772
+ ext: ".map",
773
+ mime: "application/json",
774
+ kind: "binary",
775
+ document: false,
776
+ reader: false,
777
+ editable: false
778
+ },
779
+ {
780
+ ext: ".wasm",
781
+ mime: "application/wasm",
782
+ kind: "binary",
783
+ document: false,
784
+ reader: false,
785
+ editable: false
786
+ },
787
+ {
788
+ ext: ".woff2",
789
+ mime: "font/woff2",
790
+ kind: "binary",
791
+ document: false,
792
+ reader: false,
793
+ editable: false
794
+ },
795
+ {
796
+ ext: ".woff",
797
+ mime: "font/woff",
798
+ kind: "binary",
799
+ document: false,
800
+ reader: false,
801
+ editable: false
802
+ },
803
+ {
804
+ ext: ".ttf",
805
+ mime: "font/ttf",
806
+ kind: "binary",
807
+ document: false,
808
+ reader: false,
809
+ editable: false
810
+ },
811
+ {
812
+ ext: ".mp4",
813
+ mime: "video/mp4",
814
+ kind: "binary",
815
+ document: false,
816
+ reader: false,
817
+ editable: false
818
+ },
819
+ {
820
+ ext: ".webm",
821
+ mime: "video/webm",
822
+ kind: "binary",
823
+ document: false,
824
+ reader: false,
825
+ editable: false
826
+ }
827
+ ];
828
+ function extensionsWhere(include) {
829
+ return FILE_TYPES.filter(include).map((type) => type.ext);
830
+ }
831
+ const DOCUMENT_EXTENSIONS$1 = extensionsWhere((type) => type.document);
832
+ const VIEWABLE_EXTENSIONS$1 = extensionsWhere(
833
+ (type) => type.kind !== "binary"
834
+ );
835
+ extensionsWhere((type) => type.reader);
836
+ extensionsWhere((type) => type.editable);
837
+ const WEB_ASSET_EXTENSIONS = [
838
+ ".html",
839
+ ".htm",
840
+ ".js",
841
+ ".mjs",
842
+ ".cjs",
843
+ ".css"
844
+ ];
845
+ const GALLERY_EXTENSIONS = extensionsWhere(
846
+ (type) => type.kind === "image"
847
+ );
848
+ Object.fromEntries(
849
+ FILE_TYPES.filter((type) => type.mime !== null).map((type) => [type.ext, type.mime])
850
+ );
851
+ function extensionOf(path) {
852
+ const name = path.slice(path.lastIndexOf("/") + 1);
853
+ const dot = name.lastIndexOf(".");
854
+ return dot <= 0 ? "" : name.slice(dot).toLowerCase();
855
+ }
377
856
  async function sha256Hex(bytes) {
378
857
  const digest = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
379
858
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -405,52 +884,29 @@ function shouldIgnore(path) {
405
884
  return segments.some((segment) => IGNORED_DIRECTORIES.includes(segment));
406
885
  }
407
886
  const REQUIRED_INDEX = "index.html";
408
- const DOCUMENT_EXTENSIONS = [".md", ".markdown", ".pdf"];
887
+ const DOCUMENT_EXTENSIONS = DOCUMENT_EXTENSIONS$1;
409
888
  function isDocumentPath(path) {
410
889
  const lower = path.toLowerCase();
411
890
  return DOCUMENT_EXTENSIONS.some((extension) => lower.endsWith(extension));
412
891
  }
413
- const VIEWABLE_EXTENSIONS = [
414
- // Documents — these also count at any depth, via DOCUMENT_EXTENSIONS.
415
- ".md",
416
- ".markdown",
417
- ".pdf",
418
- // Prose the reader shows preformatted.
419
- ".txt",
420
- ".log",
421
- ".csv",
422
- // Source the reader shows with highlighting.
423
- ".json",
424
- ".yaml",
425
- ".yml",
426
- ".toml",
427
- ".css",
428
- ".js",
429
- ".ts",
430
- ".tsx",
431
- ".jsx",
432
- ".sh",
433
- ".xml",
434
- // Pictures the reader shows with an img element.
435
- ".png",
436
- ".jpg",
437
- ".jpeg",
438
- ".gif",
439
- ".webp",
440
- ".avif",
441
- ".svg",
442
- ".ico",
443
- // See the note above: a floor for a caller that did not rename it.
444
- ".html",
445
- ".htm"
446
- ];
892
+ const VIEWABLE_EXTENSIONS = VIEWABLE_EXTENSIONS$1;
447
893
  function isViewablePath(path) {
448
894
  const lower = path.toLowerCase();
449
895
  return VIEWABLE_EXTENSIONS.some((extension) => lower.endsWith(extension));
450
896
  }
897
+ function isImageFolder(paths) {
898
+ let pictures = false;
899
+ for (const path of paths) {
900
+ const ext = extensionOf(path);
901
+ if (WEB_ASSET_EXTENSIONS.includes(ext)) return false;
902
+ if (GALLERY_EXTENSIONS.includes(ext)) pictures = true;
903
+ }
904
+ return pictures;
905
+ }
451
906
  function canPublish(paths) {
452
907
  if (paths.some((path) => path === REQUIRED_INDEX)) return true;
453
908
  if (paths.some(isDocumentPath)) return true;
909
+ if (isImageFolder(paths)) return true;
454
910
  return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
455
911
  }
456
912
  function renameOfLoneHtmlPage(paths) {
@@ -479,37 +935,42 @@ function checkLimits(files, limits) {
479
935
  `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.`
480
936
  );
481
937
  }
482
- if (!limits) return;
938
+ const refusal = sizeRefusal(
939
+ files.map((file) => ({ path: file.path, size: file.bytes.length })),
940
+ limits
941
+ );
942
+ if (refusal) throw refusal;
943
+ }
944
+ function sizeRefusal(files, limits) {
945
+ if (!limits) return null;
483
946
  if (files.length > limits.maxFiles) {
484
- throw new DeployError(
947
+ return new DeployError(
485
948
  ClientErrorCode.TooManyFiles,
486
949
  `That is ${files.length} files, and this plan allows ${limits.maxFiles}.`,
487
950
  { limit: limits.maxFiles, actual: files.length }
488
951
  );
489
952
  }
490
- const oversized = files.find((file) => file.bytes.length > limits.maxFileBytes);
953
+ const oversized = files.find((file) => file.size > limits.maxFileBytes);
491
954
  if (oversized) {
492
- throw new DeployError(
955
+ return new DeployError(
493
956
  ClientErrorCode.FileTooLarge,
494
- `${oversized.path} is ${formatBytes(oversized.bytes.length)}, and this plan allows ${formatBytes(limits.maxFileBytes)} per file.`,
957
+ `${oversized.path} is ${formatBytes(oversized.size)}, and this plan allows ${formatBytes(limits.maxFileBytes)} per file.`,
495
958
  {
496
959
  path: oversized.path,
497
960
  limit: limits.maxFileBytes,
498
- actual: oversized.bytes.length
961
+ actual: oversized.size
499
962
  }
500
963
  );
501
964
  }
502
- const total = totalBytes(files);
965
+ const total = files.reduce((sum, file) => sum + file.size, 0);
503
966
  if (limits.maxSiteBytes !== null && total > limits.maxSiteBytes) {
504
- throw new DeployError(
967
+ return new DeployError(
505
968
  ClientErrorCode.TooLarge,
506
969
  `That drop is ${formatBytes(total)}, and this plan allows ${formatBytes(limits.maxSiteBytes)}.`,
507
970
  { limit: limits.maxSiteBytes, actual: total }
508
971
  );
509
972
  }
510
- }
511
- function totalBytes(files) {
512
- return files.reduce((sum, file) => sum + file.bytes.length, 0);
973
+ return null;
513
974
  }
514
975
  const MAX_NAME_LENGTH = 60;
515
976
  const HEAD_BYTES = 64 * 1024;
@@ -1067,7 +1528,7 @@ async function signInWithCode(waitSeconds, replace = false) {
1067
1528
  "Call this tool again to keep waiting. Nothing has been changed yet."
1068
1529
  ].join("\n");
1069
1530
  }
1070
- const SERVER_VERSION = "0.5.1";
1531
+ const SERVER_VERSION = "0.6.0";
1071
1532
  const CREDENTIAL_WRITE = {
1072
1533
  readOnlyHint: false,
1073
1534
  destructiveHint: false,
@@ -1139,7 +1600,7 @@ function createServer() {
1139
1600
  { name: "drop2run", version: SERVER_VERSION },
1140
1601
  {
1141
1602
  instructions: [
1142
- "Publishes static sites to Drop2Run.",
1603
+ "Publishes pages, documents and static sites to Drop2Run, and returns a URL.",
1143
1604
  "",
1144
1605
  "Use publish_files for anything you wrote here — a page, a markdown note, several files",
1145
1606
  "together — and publish_dir for a folder that already exists on disk. Leave `site` unset to",
@@ -1152,9 +1613,10 @@ function createServer() {
1152
1613
  "be deleted by hand. `site` and `subdomain` are opposites: one publishes over a site that",
1153
1614
  "exists, the other creates one that does not. Never pass both.",
1154
1615
  "",
1155
- "A site needs an index.html at its top level, or at least one .md, .markdown or .pdf file,",
1156
- "which publishes as a documents site and is read through a viewer. So one page goes at",
1157
- "index.html, and a single note is a whole publish that needs no wrapping in HTML.",
1616
+ "A site needs an index.html at its top level, or at least one document (.md, .markdown,",
1617
+ ".pdf, .docx, .xlsx or .epub), which publishes as a documents site and is read through a",
1618
+ "viewer. So one page goes at index.html, and a single note is a whole publish that needs",
1619
+ "no wrapping in HTML.",
1158
1620
  "",
1159
1621
  "Publishing needs an access token. If there is none, call login — it opens a browser and",
1160
1622
  "stores one, and no command line or manual token is involved. Where no browser can be",
@@ -1226,7 +1688,7 @@ function createServer() {
1226
1688
  "publish_dir",
1227
1689
  {
1228
1690
  title: "Publish a folder",
1229
- description: "Publishes a folder of static files and returns its URL. The folder needs an index.html at its root, or .md and .pdf files, which are served through the reader.",
1691
+ description: "Publishes a folder from disk and returns its URL. The folder needs an index.html at its root, or documents (.md, .pdf, .docx, .xlsx, .epub), which are served through the reader.",
1230
1692
  inputSchema: {
1231
1693
  path: z.string().min(1).describe("Absolute path of the folder to publish."),
1232
1694
  site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@drop2run/mcp",
3
- "version": "0.5.1",
4
- "description": "MCP server that publishes a folder or an HTML page to Drop2Run and returns a live URL.",
3
+ "version": "0.6.0",
4
+ "description": "MCP server that publishes what an AI made — a page, a document or a whole site — to Drop2Run and returns a live URL.",
5
5
  "mcpName": "run.dropto/drop2run",
6
6
  "license": "MIT",
7
7
  "type": "module",