@drop2run/mcp 0.4.2 → 0.4.3

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 +1 -0
  2. package/dist/index.js +98 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,6 +66,7 @@ lose it.
66
66
  | `publish_files` | Publishes files Claude wrote — a page, a markdown note, several files together |
67
67
  | `publish_dir` | Publishes a folder, given its absolute path |
68
68
  | `list_sites` | Lists the sites on the account |
69
+ | `delete_site` | Deletes a site permanently, subdomain included — requires the subdomain repeated as `confirm` |
69
70
 
70
71
  Both publish tools take an optional `site` — a subdomain or site id to publish over. Leave it out and a
71
72
  new site is created.
package/dist/index.js CHANGED
@@ -23,6 +23,26 @@ async function call(credentials, path, init = {}) {
23
23
  }
24
24
  return await response.json();
25
25
  }
26
+ async function findSite(credentials, site) {
27
+ const wanted = site.trim().toLowerCase();
28
+ const found = (await listSites(credentials)).find(
29
+ (candidate) => candidate.siteId.toLowerCase() === wanted || candidate.subdomain.toLowerCase() === wanted
30
+ );
31
+ if (found === void 0) {
32
+ throw new Error(`No site of yours is called "${site}". \`drop2run ls\` shows what exists.`);
33
+ }
34
+ return found;
35
+ }
36
+ async function deleteSite(credentials, siteId) {
37
+ const response = await fetch(`${credentials.apiBaseUrl}/sites/${encodeURIComponent(siteId)}`, {
38
+ method: "DELETE",
39
+ headers: { Authorization: `Bearer ${credentials.token}` }
40
+ });
41
+ if (!response.ok) {
42
+ const problem = await response.json().catch(() => null);
43
+ throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
44
+ }
45
+ }
26
46
  async function listSites(credentials) {
27
47
  const body = await call(credentials, "sites");
28
48
  return body.sites;
@@ -1027,7 +1047,7 @@ async function signInWithCode(waitSeconds, replace = false) {
1027
1047
  "Call this tool again to keep waiting. Nothing has been changed yet."
1028
1048
  ].join("\n");
1029
1049
  }
1030
- const SERVER_VERSION = "0.4.2";
1050
+ const SERVER_VERSION = "0.4.3";
1031
1051
  const CREDENTIAL_WRITE = {
1032
1052
  readOnlyHint: false,
1033
1053
  destructiveHint: false,
@@ -1044,9 +1064,33 @@ const READS_ONLY = {
1044
1064
  readOnlyHint: true,
1045
1065
  openWorldHint: true
1046
1066
  };
1067
+ const PUBLISH_OUTPUT = {
1068
+ url: z.string().describe("The live URL of the published site."),
1069
+ siteId: z.string().describe("Identifier of the site, stable across publishes."),
1070
+ subdomain: z.string().describe("The site's subdomain, which is what `site` accepts."),
1071
+ files: z.number().int().describe("How many files the site now serves."),
1072
+ unchanged: z.boolean().describe(
1073
+ "True when every file already matched what the site served, so no new version was published."
1074
+ )
1075
+ };
1076
+ const REMOVES_A_SITE = {
1077
+ readOnlyHint: false,
1078
+ destructiveHint: true,
1079
+ idempotentHint: false,
1080
+ openWorldHint: true
1081
+ };
1082
+ const SITE_OUTPUT = z.object({
1083
+ siteId: z.string().describe("Identifier of the site."),
1084
+ subdomain: z.string().describe("Its subdomain, which is what `site` accepts."),
1085
+ url: z.string().describe("Its live URL."),
1086
+ name: z.string().nullable().describe("What it is called, or null when it has no name.")
1087
+ });
1047
1088
  function say(text, isError = false) {
1048
1089
  return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
1049
1090
  }
1091
+ function report(text, structured) {
1092
+ return { content: [{ type: "text", text }], structuredContent: structured };
1093
+ }
1050
1094
  async function withCredentials(work) {
1051
1095
  const credentials = loadCredentials();
1052
1096
  if (credentials === null) return say(missingCredentialsMessage("mcp"), true);
@@ -1054,7 +1098,8 @@ async function withCredentials(work) {
1054
1098
  }
1055
1099
  async function attempt(work) {
1056
1100
  try {
1057
- return say(await work());
1101
+ const answer = await work();
1102
+ return typeof answer === "string" ? say(answer) : answer;
1058
1103
  } catch (error) {
1059
1104
  return say(error instanceof Error ? error.message : String(error), true);
1060
1105
  }
@@ -1090,7 +1135,11 @@ function createServer() {
1090
1135
  "opened, login_code gives a short code to approve from another machine.",
1091
1136
  "",
1092
1137
  "Publishing over an existing site replaces what it serves, so ask before doing that to a",
1093
- "site the person did not name."
1138
+ "site the person did not name.",
1139
+ "",
1140
+ "delete_site is permanent and frees the subdomain for anybody to claim. Ask the person for",
1141
+ "the subdomain and pass what they say as `confirm` — do not fill it in from what you already",
1142
+ "know, because being told it is the point of the step."
1094
1143
  ].join("\n")
1095
1144
  }
1096
1145
  );
@@ -1136,11 +1185,13 @@ function createServer() {
1136
1185
  ).min(1).describe("The files to publish."),
1137
1186
  site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site.")
1138
1187
  },
1188
+ outputSchema: PUBLISH_OUTPUT,
1139
1189
  annotations: REPLACES_A_SITE
1140
1190
  },
1141
- ({ files, site }) => withCredentials(
1142
- async (credentials) => describe(await publishFiles(credentials, files, site))
1143
- )
1191
+ ({ files, site }) => withCredentials(async (credentials) => {
1192
+ const result = await publishFiles(credentials, files, site);
1193
+ return report(describe(result), { ...result });
1194
+ })
1144
1195
  );
1145
1196
  server.registerTool(
1146
1197
  "publish_dir",
@@ -1151,11 +1202,44 @@ function createServer() {
1151
1202
  path: z.string().min(1).describe("Absolute path of the folder to publish."),
1152
1203
  site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site.")
1153
1204
  },
1205
+ outputSchema: PUBLISH_OUTPUT,
1154
1206
  annotations: REPLACES_A_SITE
1155
1207
  },
1156
- ({ path, site }) => withCredentials(
1157
- async (credentials) => describe(await publishDirectory(credentials, path, site))
1158
- )
1208
+ ({ path, site }) => withCredentials(async (credentials) => {
1209
+ const result = await publishDirectory(credentials, path, site);
1210
+ return report(describe(result), { ...result });
1211
+ })
1212
+ );
1213
+ server.registerTool(
1214
+ "delete_site",
1215
+ {
1216
+ title: "Delete a site",
1217
+ description: "Takes a site down permanently — its files, its history and its subdomain. Nothing here undoes it, and the subdomain becomes available for anybody to claim. Requires `confirm` to repeat the site's own subdomain exactly; ask the person for it rather than filling it in from what you already know, because that is the step this asks for.",
1218
+ inputSchema: {
1219
+ site: z.string().min(1).describe("Subdomain or site id to delete."),
1220
+ confirm: z.string().min(1).describe(
1221
+ "The subdomain of the site being deleted, repeated exactly. A mismatch refuses the call rather than guessing which site was meant."
1222
+ )
1223
+ },
1224
+ outputSchema: {
1225
+ siteId: z.string().describe("Identifier of the site that was deleted."),
1226
+ subdomain: z.string().describe("Its subdomain, now unclaimed.")
1227
+ },
1228
+ annotations: REMOVES_A_SITE
1229
+ },
1230
+ ({ site, confirm }) => withCredentials(async (credentials) => {
1231
+ const found = await findSite(credentials, site);
1232
+ if (confirm.trim().toLowerCase() !== found.subdomain.toLowerCase()) {
1233
+ throw new Error(
1234
+ `Refusing to delete ${found.subdomain}: \`confirm\` said "${confirm}". Repeat the subdomain exactly to go ahead.`
1235
+ );
1236
+ }
1237
+ await deleteSite(credentials, found.siteId);
1238
+ return report(
1239
+ `Deleted ${found.subdomain}. Its files and history are gone, and the subdomain is free for anybody to claim.`,
1240
+ { siteId: found.siteId, subdomain: found.subdomain }
1241
+ );
1242
+ })
1159
1243
  );
1160
1244
  server.registerTool(
1161
1245
  "list_sites",
@@ -1163,14 +1247,15 @@ function createServer() {
1163
1247
  title: "List sites",
1164
1248
  description: "Lists the sites on this account, so a publish can go to one of them.",
1165
1249
  inputSchema: {},
1250
+ outputSchema: { sites: z.array(SITE_OUTPUT).describe("Every site on this account.") },
1166
1251
  annotations: READS_ONLY
1167
1252
  },
1168
1253
  () => withCredentials(async (credentials) => {
1169
1254
  const sites = await listSites(credentials);
1170
- if (sites.length === 0) {
1171
- return "No sites on this account yet. Publishing without a `site` creates one.";
1172
- }
1173
- return sites.map((site) => `${site.subdomain} — ${site.url}${site.name ? ` — ${site.name}` : ""}`).join("\n");
1255
+ const text = sites.length === 0 ? "No sites on this account yet. Publishing without a `site` creates one." : sites.map(
1256
+ (site) => `${site.subdomain} — ${site.url}${site.name ? ` — ${site.name}` : ""}`
1257
+ ).join("\n");
1258
+ return report(text, { sites: sites.map((site) => ({ ...site })) });
1174
1259
  })
1175
1260
  );
1176
1261
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drop2run/mcp",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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",