@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.
- package/README.md +7 -1
- package/dist/index.js +124 -18
- 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** —
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
return sites.map((site) =>
|
|
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.
|
|
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",
|