@drop2run/mcp 0.4.1 → 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 +7 -1
  2. package/dist/index.js +124 -18
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -7,13 +7,18 @@ Full documentation at [dropto.run/docs/mcp](https://dropto.run/docs/mcp).
7
7
 
8
8
  ## Setup
9
9
 
10
+ **Claude Desktop** — download [`drop2run.mcpb`](https://dropto.run/drop2run.mcpb) and open it. Claude
11
+ shows an install dialog; that is the whole of it. No terminal, no config file to edit, and nothing to
12
+ install first — Claude for macOS and Windows ships the node this runs on.
13
+
10
14
  **Claude Code** — one command:
11
15
 
12
16
  ```bash
13
17
  claude mcp add drop2run -s user -- npx -y @drop2run/mcp
14
18
  ```
15
19
 
16
- **Claude Desktop** — add this to `claude_desktop_config.json`, then restart the app:
20
+ **Claude Desktop, from npm instead** — if you would rather run the published package than the bundle,
21
+ add this to `claude_desktop_config.json` and restart the app:
17
22
 
18
23
  ```json
19
24
  {
@@ -61,6 +66,7 @@ lose it.
61
66
  | `publish_files` | Publishes files Claude wrote — a page, a markdown note, several files together |
62
67
  | `publish_dir` | Publishes a folder, given its absolute path |
63
68
  | `list_sites` | Lists the sites on the account |
69
+ | `delete_site` | Deletes a site permanently, subdomain included — requires the subdomain repeated as `confirm` |
64
70
 
65
71
  Both publish tools take an optional `site` — a subdomain or site id to publish over. Leave it out and a
66
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,10 +1047,50 @@ 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.1";
1050
+ const SERVER_VERSION = "0.4.3";
1051
+ const CREDENTIAL_WRITE = {
1052
+ readOnlyHint: false,
1053
+ destructiveHint: false,
1054
+ idempotentHint: false,
1055
+ openWorldHint: true
1056
+ };
1057
+ const REPLACES_A_SITE = {
1058
+ readOnlyHint: false,
1059
+ destructiveHint: true,
1060
+ idempotentHint: false,
1061
+ openWorldHint: true
1062
+ };
1063
+ const READS_ONLY = {
1064
+ readOnlyHint: true,
1065
+ openWorldHint: true
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
+ });
1031
1088
  function say(text, isError = false) {
1032
1089
  return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
1033
1090
  }
1091
+ function report(text, structured) {
1092
+ return { content: [{ type: "text", text }], structuredContent: structured };
1093
+ }
1034
1094
  async function withCredentials(work) {
1035
1095
  const credentials = loadCredentials();
1036
1096
  if (credentials === null) return say(missingCredentialsMessage("mcp"), true);
@@ -1038,7 +1098,8 @@ async function withCredentials(work) {
1038
1098
  }
1039
1099
  async function attempt(work) {
1040
1100
  try {
1041
- return say(await work());
1101
+ const answer = await work();
1102
+ return typeof answer === "string" ? say(answer) : answer;
1042
1103
  } catch (error) {
1043
1104
  return say(error instanceof Error ? error.message : String(error), true);
1044
1105
  }
@@ -1074,7 +1135,11 @@ function createServer() {
1074
1135
  "opened, login_code gives a short code to approve from another machine.",
1075
1136
  "",
1076
1137
  "Publishing over an existing site replaces what it serves, so ask before doing that to a",
1077
- "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."
1078
1143
  ].join("\n")
1079
1144
  }
1080
1145
  );
@@ -1088,7 +1153,8 @@ function createServer() {
1088
1153
  replace: z.boolean().optional().describe(
1089
1154
  "Sign in even though a token is already stored. Use when switching accounts, or when the stored token is being refused."
1090
1155
  )
1091
- }
1156
+ },
1157
+ annotations: CREDENTIAL_WRITE
1092
1158
  },
1093
1159
  ({ waitSeconds, replace }) => attempt(() => signInWithBrowser(waitSeconds, replace))
1094
1160
  );
@@ -1100,7 +1166,8 @@ function createServer() {
1100
1166
  inputSchema: {
1101
1167
  waitSeconds: z.number().int().min(5).max(MAX_WAIT_SECONDS).optional().describe("How long a waiting call polls before answering. Default 120."),
1102
1168
  replace: z.boolean().optional().describe("Sign in even though a token is already stored.")
1103
- }
1169
+ },
1170
+ annotations: CREDENTIAL_WRITE
1104
1171
  },
1105
1172
  ({ waitSeconds, replace }) => attempt(() => signInWithCode(waitSeconds, replace))
1106
1173
  );
@@ -1117,11 +1184,14 @@ function createServer() {
1117
1184
  })
1118
1185
  ).min(1).describe("The files to publish."),
1119
1186
  site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site.")
1120
- }
1187
+ },
1188
+ outputSchema: PUBLISH_OUTPUT,
1189
+ annotations: REPLACES_A_SITE
1121
1190
  },
1122
- ({ files, site }) => withCredentials(
1123
- async (credentials) => describe(await publishFiles(credentials, files, site))
1124
- )
1191
+ ({ files, site }) => withCredentials(async (credentials) => {
1192
+ const result = await publishFiles(credentials, files, site);
1193
+ return report(describe(result), { ...result });
1194
+ })
1125
1195
  );
1126
1196
  server.registerTool(
1127
1197
  "publish_dir",
@@ -1131,25 +1201,61 @@ function createServer() {
1131
1201
  inputSchema: {
1132
1202
  path: z.string().min(1).describe("Absolute path of the folder to publish."),
1133
1203
  site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site.")
1134
- }
1204
+ },
1205
+ outputSchema: PUBLISH_OUTPUT,
1206
+ annotations: REPLACES_A_SITE
1135
1207
  },
1136
- ({ path, site }) => withCredentials(
1137
- async (credentials) => describe(await publishDirectory(credentials, path, site))
1138
- )
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
+ })
1139
1243
  );
1140
1244
  server.registerTool(
1141
1245
  "list_sites",
1142
1246
  {
1143
1247
  title: "List sites",
1144
1248
  description: "Lists the sites on this account, so a publish can go to one of them.",
1145
- inputSchema: {}
1249
+ inputSchema: {},
1250
+ outputSchema: { sites: z.array(SITE_OUTPUT).describe("Every site on this account.") },
1251
+ annotations: READS_ONLY
1146
1252
  },
1147
1253
  () => withCredentials(async (credentials) => {
1148
1254
  const sites = await listSites(credentials);
1149
- if (sites.length === 0) {
1150
- return "No sites on this account yet. Publishing without a `site` creates one.";
1151
- }
1152
- 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 })) });
1153
1259
  })
1154
1260
  );
1155
1261
  return server;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drop2run/mcp",
3
- "version": "0.4.1",
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",
@@ -42,7 +42,8 @@
42
42
  "test": "vitest run",
43
43
  "lint": "biome check .",
44
44
  "format": "biome check --write .",
45
- "build": "vite build"
45
+ "build": "vite build",
46
+ "build:mcpb": "node scripts/build-mcpb.mjs"
46
47
  },
47
48
  "dependencies": {
48
49
  "@modelcontextprotocol/sdk": "^1.30.0",