@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.
- package/README.md +1 -0
- package/dist/index.js +98 -13
- 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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
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 })) });
|
|
1174
1259
|
})
|
|
1175
1260
|
);
|
|
1176
1261
|
return server;
|