@drop2run/mcp 0.4.2 → 0.5.0
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 +10 -1
- package/dist/index.js +142 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,10 +66,17 @@ 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.
|
|
72
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
|
+
|
|
73
80
|
Both sign-in tools answer "not approved yet" rather than failing while they wait, and calling them
|
|
74
81
|
again keeps waiting on the same sign-in. Neither replaces a token that is already stored unless asked
|
|
75
82
|
to with `replace`.
|
|
@@ -77,7 +84,9 @@ to with `replace`.
|
|
|
77
84
|
## Behaviour worth knowing
|
|
78
85
|
|
|
79
86
|
**Publishing without a `site` creates a new one.** It never replaces your most recent site by default:
|
|
80
|
-
"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.
|
|
81
90
|
|
|
82
91
|
**A site can be documents instead of a built site.** A publish needs an `index.html` at the top level,
|
|
83
92
|
or at least one `.md`, `.markdown` or `.pdf` file — those are served through the reader. So a single
|
package/dist/index.js
CHANGED
|
@@ -23,15 +23,41 @@ 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;
|
|
29
49
|
}
|
|
30
|
-
async function createSite(credentials) {
|
|
50
|
+
async function createSite(credentials, subdomain) {
|
|
31
51
|
const created = await call(
|
|
32
52
|
credentials,
|
|
33
53
|
"sites",
|
|
34
|
-
{
|
|
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
|
+
}
|
|
35
61
|
);
|
|
36
62
|
return { ...created, name: null };
|
|
37
63
|
}
|
|
@@ -284,6 +310,7 @@ const ClientErrorCode = {
|
|
|
284
310
|
/** An upload kept failing after every retry. */
|
|
285
311
|
UploadFailed: "upload_failed"
|
|
286
312
|
};
|
|
313
|
+
const ACCOUNT_HEADER = "X-Drop2Run-Account";
|
|
287
314
|
const CLAIM_TOKEN_HEADER = "X-Claim-Token";
|
|
288
315
|
async function prepareDeploy(siteId, files, options, signal) {
|
|
289
316
|
const manifest = files.map((file) => ({
|
|
@@ -320,6 +347,7 @@ async function request(path, body, options, signal) {
|
|
|
320
347
|
if (body !== void 0) headers["Content-Type"] = "application/json";
|
|
321
348
|
if (options.claimToken) headers[CLAIM_TOKEN_HEADER] = options.claimToken;
|
|
322
349
|
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
350
|
+
if (options.accountId) headers[ACCOUNT_HEADER] = options.accountId;
|
|
323
351
|
const response = await doFetch(`${base}/${path}`, {
|
|
324
352
|
method: "POST",
|
|
325
353
|
headers,
|
|
@@ -613,7 +641,7 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
|
|
|
613
641
|
});
|
|
614
642
|
done += 1;
|
|
615
643
|
uploadedBytes += file.bytes.length;
|
|
616
|
-
onProgress?.(done, targets.length, uploadedBytes);
|
|
644
|
+
onProgress?.(done, targets.length, uploadedBytes, file.path);
|
|
617
645
|
}
|
|
618
646
|
};
|
|
619
647
|
const workers = Array.from(
|
|
@@ -708,12 +736,13 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
708
736
|
prepared.upload,
|
|
709
737
|
prepared.uploadUrl,
|
|
710
738
|
hashed,
|
|
711
|
-
(done, _total, bytes) => onProgress({
|
|
739
|
+
(done, _total, bytes, path) => onProgress({
|
|
712
740
|
type: "uploading",
|
|
713
741
|
done: prepared.reused + done,
|
|
714
742
|
total: prepared.total,
|
|
715
743
|
bytes,
|
|
716
|
-
reused: prepared.reused
|
|
744
|
+
reused: prepared.reused,
|
|
745
|
+
path
|
|
717
746
|
}),
|
|
718
747
|
signal,
|
|
719
748
|
options.upload
|
|
@@ -779,7 +808,15 @@ function escapesRoot(root, candidate) {
|
|
|
779
808
|
const inside = relative(root, candidate);
|
|
780
809
|
return inside === "" || inside.startsWith("..");
|
|
781
810
|
}
|
|
782
|
-
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
|
+
}
|
|
783
820
|
if (site === void 0) return await createSite(credentials);
|
|
784
821
|
const wanted = site.trim().toLowerCase();
|
|
785
822
|
const found = (await listSites(credentials)).find(
|
|
@@ -787,13 +824,13 @@ async function resolveSite(credentials, site) {
|
|
|
787
824
|
);
|
|
788
825
|
if (found === void 0) {
|
|
789
826
|
throw new Error(
|
|
790
|
-
`No site of yours is called "${site}". Leave the site out to publish to a new one, or
|
|
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.`
|
|
791
828
|
);
|
|
792
829
|
}
|
|
793
830
|
return found;
|
|
794
831
|
}
|
|
795
|
-
async function publish(credentials, source, site) {
|
|
796
|
-
const target = await resolveSite(credentials, site);
|
|
832
|
+
async function publish(credentials, source, site, onProgress, subdomain) {
|
|
833
|
+
const target = await resolveSite(credentials, site, subdomain);
|
|
797
834
|
let files = 0;
|
|
798
835
|
let url = target.url;
|
|
799
836
|
let unchanged = false;
|
|
@@ -823,8 +860,8 @@ async function publish(credentials, source, site) {
|
|
|
823
860
|
});
|
|
824
861
|
return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
|
|
825
862
|
}
|
|
826
|
-
function publishDirectory(credentials, directory, site) {
|
|
827
|
-
return publish(credentials, directorySource(directory), site);
|
|
863
|
+
function publishDirectory(credentials, directory, site, onProgress, subdomain) {
|
|
864
|
+
return publish(credentials, directorySource(directory), site, onProgress, subdomain);
|
|
828
865
|
}
|
|
829
866
|
function cleanPath(path) {
|
|
830
867
|
const cleaned = path.trim().replaceAll("\\", "/").replace(/^\.?\//, "");
|
|
@@ -838,7 +875,7 @@ function cleanPath(path) {
|
|
|
838
875
|
}
|
|
839
876
|
return cleaned;
|
|
840
877
|
}
|
|
841
|
-
async function publishFiles(credentials, files, site) {
|
|
878
|
+
async function publishFiles(credentials, files, site, subdomain) {
|
|
842
879
|
const encoder = new TextEncoder();
|
|
843
880
|
const collected = files.map((file) => ({
|
|
844
881
|
path: cleanPath(file.path),
|
|
@@ -849,7 +886,7 @@ async function publishFiles(credentials, files, site) {
|
|
|
849
886
|
if (seen.has(file.path)) throw new Error(`Two files were given the same path: ${file.path}`);
|
|
850
887
|
seen.add(file.path);
|
|
851
888
|
}
|
|
852
|
-
return publish(credentials, async () => collected, site);
|
|
889
|
+
return publish(credentials, async () => collected, site, void 0, subdomain);
|
|
853
890
|
}
|
|
854
891
|
const DEFAULT_WAIT_SECONDS = 120;
|
|
855
892
|
const MAX_WAIT_SECONDS = 240;
|
|
@@ -1027,7 +1064,7 @@ async function signInWithCode(waitSeconds, replace = false) {
|
|
|
1027
1064
|
"Call this tool again to keep waiting. Nothing has been changed yet."
|
|
1028
1065
|
].join("\n");
|
|
1029
1066
|
}
|
|
1030
|
-
const SERVER_VERSION = "0.
|
|
1067
|
+
const SERVER_VERSION = "0.5.0";
|
|
1031
1068
|
const CREDENTIAL_WRITE = {
|
|
1032
1069
|
readOnlyHint: false,
|
|
1033
1070
|
destructiveHint: false,
|
|
@@ -1044,9 +1081,33 @@ const READS_ONLY = {
|
|
|
1044
1081
|
readOnlyHint: true,
|
|
1045
1082
|
openWorldHint: true
|
|
1046
1083
|
};
|
|
1084
|
+
const PUBLISH_OUTPUT = {
|
|
1085
|
+
url: z.string().describe("The live URL of the published site."),
|
|
1086
|
+
siteId: z.string().describe("Identifier of the site, stable across publishes."),
|
|
1087
|
+
subdomain: z.string().describe("The site's subdomain, which is what `site` accepts."),
|
|
1088
|
+
files: z.number().int().describe("How many files the site now serves."),
|
|
1089
|
+
unchanged: z.boolean().describe(
|
|
1090
|
+
"True when every file already matched what the site served, so no new version was published."
|
|
1091
|
+
)
|
|
1092
|
+
};
|
|
1093
|
+
const REMOVES_A_SITE = {
|
|
1094
|
+
readOnlyHint: false,
|
|
1095
|
+
destructiveHint: true,
|
|
1096
|
+
idempotentHint: false,
|
|
1097
|
+
openWorldHint: true
|
|
1098
|
+
};
|
|
1099
|
+
const SITE_OUTPUT = z.object({
|
|
1100
|
+
siteId: z.string().describe("Identifier of the site."),
|
|
1101
|
+
subdomain: z.string().describe("Its subdomain, which is what `site` accepts."),
|
|
1102
|
+
url: z.string().describe("Its live URL."),
|
|
1103
|
+
name: z.string().nullable().describe("What it is called, or null when it has no name.")
|
|
1104
|
+
});
|
|
1047
1105
|
function say(text, isError = false) {
|
|
1048
1106
|
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
|
|
1049
1107
|
}
|
|
1108
|
+
function report(text, structured) {
|
|
1109
|
+
return { content: [{ type: "text", text }], structuredContent: structured };
|
|
1110
|
+
}
|
|
1050
1111
|
async function withCredentials(work) {
|
|
1051
1112
|
const credentials = loadCredentials();
|
|
1052
1113
|
if (credentials === null) return say(missingCredentialsMessage("mcp"), true);
|
|
@@ -1054,7 +1115,8 @@ async function withCredentials(work) {
|
|
|
1054
1115
|
}
|
|
1055
1116
|
async function attempt(work) {
|
|
1056
1117
|
try {
|
|
1057
|
-
|
|
1118
|
+
const answer = await work();
|
|
1119
|
+
return typeof answer === "string" ? say(answer) : answer;
|
|
1058
1120
|
} catch (error) {
|
|
1059
1121
|
return say(error instanceof Error ? error.message : String(error), true);
|
|
1060
1122
|
}
|
|
@@ -1081,6 +1143,12 @@ function createServer() {
|
|
|
1081
1143
|
"publish to a brand-new site; pass a subdomain to publish over an existing one. list_sites",
|
|
1082
1144
|
"shows what already exists.",
|
|
1083
1145
|
"",
|
|
1146
|
+
"A new site gets a generated name unless `subdomain` asks for one. Pass it when the person",
|
|
1147
|
+
"named the address they want, and otherwise leave it out — a name is what somebody will be",
|
|
1148
|
+
"given as a URL, it cannot be changed later, and a site created under a guessed one has to",
|
|
1149
|
+
"be deleted by hand. `site` and `subdomain` are opposites: one publishes over a site that",
|
|
1150
|
+
"exists, the other creates one that does not. Never pass both.",
|
|
1151
|
+
"",
|
|
1084
1152
|
"A site needs an index.html at its top level, or at least one .md, .markdown or .pdf file,",
|
|
1085
1153
|
"which publishes as a documents site and is read through a viewer. So one page goes at",
|
|
1086
1154
|
"index.html, and a single note is a whole publish that needs no wrapping in HTML.",
|
|
@@ -1090,7 +1158,11 @@ function createServer() {
|
|
|
1090
1158
|
"opened, login_code gives a short code to approve from another machine.",
|
|
1091
1159
|
"",
|
|
1092
1160
|
"Publishing over an existing site replaces what it serves, so ask before doing that to a",
|
|
1093
|
-
"site the person did not name."
|
|
1161
|
+
"site the person did not name.",
|
|
1162
|
+
"",
|
|
1163
|
+
"delete_site is permanent and frees the subdomain for anybody to claim. Ask the person for",
|
|
1164
|
+
"the subdomain and pass what they say as `confirm` — do not fill it in from what you already",
|
|
1165
|
+
"know, because being told it is the point of the step."
|
|
1094
1166
|
].join("\n")
|
|
1095
1167
|
}
|
|
1096
1168
|
);
|
|
@@ -1134,13 +1206,18 @@ function createServer() {
|
|
|
1134
1206
|
content: z.string().describe("The file's contents.")
|
|
1135
1207
|
})
|
|
1136
1208
|
).min(1).describe("The files to publish."),
|
|
1137
|
-
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site.")
|
|
1209
|
+
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
|
|
1210
|
+
subdomain: z.string().optional().describe(
|
|
1211
|
+
"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."
|
|
1212
|
+
)
|
|
1138
1213
|
},
|
|
1214
|
+
outputSchema: PUBLISH_OUTPUT,
|
|
1139
1215
|
annotations: REPLACES_A_SITE
|
|
1140
1216
|
},
|
|
1141
|
-
({ files, site }) => withCredentials(
|
|
1142
|
-
|
|
1143
|
-
|
|
1217
|
+
({ files, site, subdomain }) => withCredentials(async (credentials) => {
|
|
1218
|
+
const result = await publishFiles(credentials, files, site, subdomain);
|
|
1219
|
+
return report(describe(result), { ...result });
|
|
1220
|
+
})
|
|
1144
1221
|
);
|
|
1145
1222
|
server.registerTool(
|
|
1146
1223
|
"publish_dir",
|
|
@@ -1149,13 +1226,49 @@ function createServer() {
|
|
|
1149
1226
|
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.",
|
|
1150
1227
|
inputSchema: {
|
|
1151
1228
|
path: z.string().min(1).describe("Absolute path of the folder to publish."),
|
|
1152
|
-
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site.")
|
|
1229
|
+
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
|
|
1230
|
+
subdomain: z.string().optional().describe(
|
|
1231
|
+
"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."
|
|
1232
|
+
)
|
|
1153
1233
|
},
|
|
1234
|
+
outputSchema: PUBLISH_OUTPUT,
|
|
1154
1235
|
annotations: REPLACES_A_SITE
|
|
1155
1236
|
},
|
|
1156
|
-
({ path, site }) => withCredentials(
|
|
1157
|
-
|
|
1158
|
-
|
|
1237
|
+
({ path, site, subdomain }) => withCredentials(async (credentials) => {
|
|
1238
|
+
const result = await publishDirectory(credentials, path, site, void 0, subdomain);
|
|
1239
|
+
return report(describe(result), { ...result });
|
|
1240
|
+
})
|
|
1241
|
+
);
|
|
1242
|
+
server.registerTool(
|
|
1243
|
+
"delete_site",
|
|
1244
|
+
{
|
|
1245
|
+
title: "Delete a site",
|
|
1246
|
+
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.",
|
|
1247
|
+
inputSchema: {
|
|
1248
|
+
site: z.string().min(1).describe("Subdomain or site id to delete."),
|
|
1249
|
+
confirm: z.string().min(1).describe(
|
|
1250
|
+
"The subdomain of the site being deleted, repeated exactly. A mismatch refuses the call rather than guessing which site was meant."
|
|
1251
|
+
)
|
|
1252
|
+
},
|
|
1253
|
+
outputSchema: {
|
|
1254
|
+
siteId: z.string().describe("Identifier of the site that was deleted."),
|
|
1255
|
+
subdomain: z.string().describe("Its subdomain, now unclaimed.")
|
|
1256
|
+
},
|
|
1257
|
+
annotations: REMOVES_A_SITE
|
|
1258
|
+
},
|
|
1259
|
+
({ site, confirm }) => withCredentials(async (credentials) => {
|
|
1260
|
+
const found = await findSite(credentials, site);
|
|
1261
|
+
if (confirm.trim().toLowerCase() !== found.subdomain.toLowerCase()) {
|
|
1262
|
+
throw new Error(
|
|
1263
|
+
`Refusing to delete ${found.subdomain}: \`confirm\` said "${confirm}". Repeat the subdomain exactly to go ahead.`
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
await deleteSite(credentials, found.siteId);
|
|
1267
|
+
return report(
|
|
1268
|
+
`Deleted ${found.subdomain}. Its files and history are gone, and the subdomain is free for anybody to claim.`,
|
|
1269
|
+
{ siteId: found.siteId, subdomain: found.subdomain }
|
|
1270
|
+
);
|
|
1271
|
+
})
|
|
1159
1272
|
);
|
|
1160
1273
|
server.registerTool(
|
|
1161
1274
|
"list_sites",
|
|
@@ -1163,14 +1276,15 @@ function createServer() {
|
|
|
1163
1276
|
title: "List sites",
|
|
1164
1277
|
description: "Lists the sites on this account, so a publish can go to one of them.",
|
|
1165
1278
|
inputSchema: {},
|
|
1279
|
+
outputSchema: { sites: z.array(SITE_OUTPUT).describe("Every site on this account.") },
|
|
1166
1280
|
annotations: READS_ONLY
|
|
1167
1281
|
},
|
|
1168
1282
|
() => withCredentials(async (credentials) => {
|
|
1169
1283
|
const sites = await listSites(credentials);
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
return sites.map((site) =>
|
|
1284
|
+
const text = sites.length === 0 ? "No sites on this account yet. Publishing without a `site` creates one." : sites.map(
|
|
1285
|
+
(site) => `${site.subdomain} — ${site.url}${site.name ? ` — ${site.name}` : ""}`
|
|
1286
|
+
).join("\n");
|
|
1287
|
+
return report(text, { sites: sites.map((site) => ({ ...site })) });
|
|
1174
1288
|
})
|
|
1175
1289
|
);
|
|
1176
1290
|
return server;
|