@drop2run/mcp 0.4.3 → 0.5.1

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 +9 -1
  2. package/dist/index.js +52 -20
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -71,6 +71,12 @@ lose it.
71
71
  Both publish tools take an optional `site` — a subdomain or site id to publish over. Leave it out and a
72
72
  new site is created.
73
73
 
74
+ Both also take an optional `subdomain`, which names the new site instead of letting the server generate
75
+ one. It is for a person who asked for a particular address: the name becomes the URL, cannot be changed
76
+ afterwards, and a site made under a guessed one has to be deleted by hand. `site` and `subdomain` are
77
+ opposites — one publishes over a site that exists, the other creates one that does not — so passing both
78
+ is refused rather than resolved.
79
+
74
80
  Both sign-in tools answer "not approved yet" rather than failing while they wait, and calling them
75
81
  again keeps waiting on the same sign-in. Neither replaces a token that is already stored unless asked
76
82
  to with `replace`.
@@ -78,7 +84,9 @@ to with `replace`.
78
84
  ## Behaviour worth knowing
79
85
 
80
86
  **Publishing without a `site` creates a new one.** It never replaces your most recent site by default:
81
- "put this online" is not "and overwrite what I published last time".
87
+ "put this online" is not "and overwrite what I published last time". Say what address you want and pass
88
+ it as `subdomain`; say nothing and the name is generated, which is the right default for something
89
+ nobody is going to link to.
82
90
 
83
91
  **A site can be documents instead of a built site.** A publish needs an `index.html` at the top level,
84
92
  or at least one `.md`, `.markdown` or `.pdf` file — those are served through the reader. So a single
package/dist/index.js CHANGED
@@ -47,11 +47,17 @@ async function listSites(credentials) {
47
47
  const body = await call(credentials, "sites");
48
48
  return body.sites;
49
49
  }
50
- async function createSite(credentials) {
50
+ async function createSite(credentials, subdomain) {
51
51
  const created = await call(
52
52
  credentials,
53
53
  "sites",
54
- { method: "POST", body: JSON.stringify({}) }
54
+ {
55
+ method: "POST",
56
+ // An absent field rather than a null one, because the API reads null, empty and missing the
57
+ // same way and a body carrying only what was asked for is the one that reads correctly in a
58
+ // log.
59
+ body: JSON.stringify(subdomain === void 0 ? {} : { subdomain })
60
+ }
55
61
  );
56
62
  return { ...created, name: null };
57
63
  }
@@ -304,6 +310,7 @@ const ClientErrorCode = {
304
310
  /** An upload kept failing after every retry. */
305
311
  UploadFailed: "upload_failed"
306
312
  };
313
+ const ACCOUNT_HEADER = "X-Drop2Run-Account";
307
314
  const CLAIM_TOKEN_HEADER = "X-Claim-Token";
308
315
  async function prepareDeploy(siteId, files, options, signal) {
309
316
  const manifest = files.map((file) => ({
@@ -340,6 +347,7 @@ async function request(path, body, options, signal) {
340
347
  if (body !== void 0) headers["Content-Type"] = "application/json";
341
348
  if (options.claimToken) headers[CLAIM_TOKEN_HEADER] = options.claimToken;
342
349
  if (options.token) headers.Authorization = `Bearer ${options.token}`;
350
+ if (options.accountId) headers[ACCOUNT_HEADER] = options.accountId;
343
351
  const response = await doFetch(`${base}/${path}`, {
344
352
  method: "POST",
345
353
  headers,
@@ -633,7 +641,7 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
633
641
  });
634
642
  done += 1;
635
643
  uploadedBytes += file.bytes.length;
636
- onProgress?.(done, targets.length, uploadedBytes);
644
+ onProgress?.(done, targets.length, uploadedBytes, file.path);
637
645
  }
638
646
  };
639
647
  const workers = Array.from(
@@ -728,12 +736,13 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
728
736
  prepared.upload,
729
737
  prepared.uploadUrl,
730
738
  hashed,
731
- (done, _total, bytes) => onProgress({
739
+ (done, _total, bytes, path) => onProgress({
732
740
  type: "uploading",
733
741
  done: prepared.reused + done,
734
742
  total: prepared.total,
735
743
  bytes,
736
- reused: prepared.reused
744
+ reused: prepared.reused,
745
+ path
737
746
  }),
738
747
  signal,
739
748
  options.upload
@@ -799,7 +808,15 @@ function escapesRoot(root, candidate) {
799
808
  const inside = relative(root, candidate);
800
809
  return inside === "" || inside.startsWith("..");
801
810
  }
802
- async function resolveSite(credentials, site) {
811
+ async function resolveSite(credentials, site, subdomain) {
812
+ if (subdomain !== void 0) {
813
+ if (site !== void 0) {
814
+ throw new Error(
815
+ "Give either an existing site or a subdomain for a new one, not both: publishing over a site and creating one are different requests."
816
+ );
817
+ }
818
+ return await createSite(credentials, subdomain);
819
+ }
803
820
  if (site === void 0) return await createSite(credentials);
804
821
  const wanted = site.trim().toLowerCase();
805
822
  const found = (await listSites(credentials)).find(
@@ -807,13 +824,13 @@ async function resolveSite(credentials, site) {
807
824
  );
808
825
  if (found === void 0) {
809
826
  throw new Error(
810
- `No site of yours is called "${site}". Leave the site out to publish to a new one, or use list_sites to see what exists.`
827
+ `No site of yours is called "${site}". Leave the site out to publish to a new one, or list your sites to see what exists.`
811
828
  );
812
829
  }
813
830
  return found;
814
831
  }
815
- async function publish(credentials, source, site) {
816
- const target = await resolveSite(credentials, site);
832
+ async function publish(credentials, source, site, onProgress, subdomain) {
833
+ const target = await resolveSite(credentials, site, subdomain);
817
834
  let files = 0;
818
835
  let url = target.url;
819
836
  let unchanged = false;
@@ -843,8 +860,11 @@ async function publish(credentials, source, site) {
843
860
  });
844
861
  return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
845
862
  }
846
- function publishDirectory(credentials, directory, site) {
847
- return publish(credentials, directorySource(directory), site);
863
+ async function publishDirectory(credentials, directory, site, onProgress, subdomain) {
864
+ const found = await stat(directory).catch(() => null);
865
+ if (found === null) throw new Error(`There is no folder at ${directory}.`);
866
+ if (!found.isDirectory()) throw new Error(`${directory} is a file, not a folder to publish.`);
867
+ return await publish(credentials, directorySource(directory), site, onProgress, subdomain);
848
868
  }
849
869
  function cleanPath(path) {
850
870
  const cleaned = path.trim().replaceAll("\\", "/").replace(/^\.?\//, "");
@@ -858,7 +878,7 @@ function cleanPath(path) {
858
878
  }
859
879
  return cleaned;
860
880
  }
861
- async function publishFiles(credentials, files, site) {
881
+ async function publishFiles(credentials, files, site, subdomain) {
862
882
  const encoder = new TextEncoder();
863
883
  const collected = files.map((file) => ({
864
884
  path: cleanPath(file.path),
@@ -869,7 +889,7 @@ async function publishFiles(credentials, files, site) {
869
889
  if (seen.has(file.path)) throw new Error(`Two files were given the same path: ${file.path}`);
870
890
  seen.add(file.path);
871
891
  }
872
- return publish(credentials, async () => collected, site);
892
+ return publish(credentials, async () => collected, site, void 0, subdomain);
873
893
  }
874
894
  const DEFAULT_WAIT_SECONDS = 120;
875
895
  const MAX_WAIT_SECONDS = 240;
@@ -1047,7 +1067,7 @@ async function signInWithCode(waitSeconds, replace = false) {
1047
1067
  "Call this tool again to keep waiting. Nothing has been changed yet."
1048
1068
  ].join("\n");
1049
1069
  }
1050
- const SERVER_VERSION = "0.4.3";
1070
+ const SERVER_VERSION = "0.5.1";
1051
1071
  const CREDENTIAL_WRITE = {
1052
1072
  readOnlyHint: false,
1053
1073
  destructiveHint: false,
@@ -1126,6 +1146,12 @@ function createServer() {
1126
1146
  "publish to a brand-new site; pass a subdomain to publish over an existing one. list_sites",
1127
1147
  "shows what already exists.",
1128
1148
  "",
1149
+ "A new site gets a generated name unless `subdomain` asks for one. Pass it when the person",
1150
+ "named the address they want, and otherwise leave it out — a name is what somebody will be",
1151
+ "given as a URL, it cannot be changed later, and a site created under a guessed one has to",
1152
+ "be deleted by hand. `site` and `subdomain` are opposites: one publishes over a site that",
1153
+ "exists, the other creates one that does not. Never pass both.",
1154
+ "",
1129
1155
  "A site needs an index.html at its top level, or at least one .md, .markdown or .pdf file,",
1130
1156
  "which publishes as a documents site and is read through a viewer. So one page goes at",
1131
1157
  "index.html, and a single note is a whole publish that needs no wrapping in HTML.",
@@ -1183,13 +1209,16 @@ function createServer() {
1183
1209
  content: z.string().describe("The file's contents.")
1184
1210
  })
1185
1211
  ).min(1).describe("The files to publish."),
1186
- site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site.")
1212
+ site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
1213
+ subdomain: z.string().optional().describe(
1214
+ "Subdomain to create the new site under, when the person asked for a particular address. Only for a new site: leave it out to publish over an existing one, and do not pass it together with `site`. Ask rather than inventing one — the name is the URL somebody will be given, it cannot be changed afterwards, and an unused site made under a guessed name has to be deleted by hand."
1215
+ )
1187
1216
  },
1188
1217
  outputSchema: PUBLISH_OUTPUT,
1189
1218
  annotations: REPLACES_A_SITE
1190
1219
  },
1191
- ({ files, site }) => withCredentials(async (credentials) => {
1192
- const result = await publishFiles(credentials, files, site);
1220
+ ({ files, site, subdomain }) => withCredentials(async (credentials) => {
1221
+ const result = await publishFiles(credentials, files, site, subdomain);
1193
1222
  return report(describe(result), { ...result });
1194
1223
  })
1195
1224
  );
@@ -1200,13 +1229,16 @@ function createServer() {
1200
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.",
1201
1230
  inputSchema: {
1202
1231
  path: z.string().min(1).describe("Absolute path of the folder to publish."),
1203
- site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site.")
1232
+ site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
1233
+ subdomain: z.string().optional().describe(
1234
+ "Subdomain to create the new site under, when the person asked for a particular address. Only for a new site: leave it out to publish over an existing one, and do not pass it together with `site`. Ask rather than inventing one — the name is the URL somebody will be given, it cannot be changed afterwards, and an unused site made under a guessed name has to be deleted by hand."
1235
+ )
1204
1236
  },
1205
1237
  outputSchema: PUBLISH_OUTPUT,
1206
1238
  annotations: REPLACES_A_SITE
1207
1239
  },
1208
- ({ path, site }) => withCredentials(async (credentials) => {
1209
- const result = await publishDirectory(credentials, path, site);
1240
+ ({ path, site, subdomain }) => withCredentials(async (credentials) => {
1241
+ const result = await publishDirectory(credentials, path, site, void 0, subdomain);
1210
1242
  return report(describe(result), { ...result });
1211
1243
  })
1212
1244
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drop2run/mcp",
3
- "version": "0.4.3",
3
+ "version": "0.5.1",
4
4
  "description": "MCP server that publishes a folder or an HTML page to Drop2Run and returns a live URL.",
5
5
  "mcpName": "run.dropto/drop2run",
6
6
  "license": "MIT",