@drop2run/mcp 0.5.1 → 0.7.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 +12 -4
- package/dist/index.js +710 -134
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# @drop2run/mcp
|
|
2
2
|
|
|
3
|
-
An MCP server that lets Claude — or any MCP client — publish
|
|
4
|
-
[Drop2Run](https://dropto.run)
|
|
3
|
+
An MCP server that lets Claude — or any MCP client — publish what it made to
|
|
4
|
+
[Drop2Run](https://dropto.run): an HTML page, a markdown note, a folder of documents or a built site. It
|
|
5
|
+
returns a live HTTPS URL.
|
|
5
6
|
|
|
6
7
|
Full documentation at [dropto.run/docs/mcp](https://dropto.run/docs/mcp).
|
|
7
8
|
|
|
@@ -66,6 +67,7 @@ lose it.
|
|
|
66
67
|
| `publish_files` | Publishes files Claude wrote — a page, a markdown note, several files together |
|
|
67
68
|
| `publish_dir` | Publishes a folder, given its absolute path |
|
|
68
69
|
| `list_sites` | Lists the sites on the account |
|
|
70
|
+
| `list_folders` | Lists the account's folders, each with the path `folder` accepts |
|
|
69
71
|
| `delete_site` | Deletes a site permanently, subdomain included — requires the subdomain repeated as `confirm` |
|
|
70
72
|
|
|
71
73
|
Both publish tools take an optional `site` — a subdomain or site id to publish over. Leave it out and a
|
|
@@ -77,6 +79,11 @@ afterwards, and a site made under a guessed one has to be deleted by hand. `site
|
|
|
77
79
|
opposites — one publishes over a site that exists, the other creates one that does not — so passing both
|
|
78
80
|
is refused rather than resolved.
|
|
79
81
|
|
|
82
|
+
And both take an optional `folder`, which files the new site in one of the account's dashboard folders —
|
|
83
|
+
by path such as `Clients/Acme`, or by the id `list_folders` shows. Like `subdomain` it only describes a new
|
|
84
|
+
site, so it cannot be combined with `site`. It never creates a folder: one that does not exist is refused
|
|
85
|
+
with the list of those that do, and nothing is published.
|
|
86
|
+
|
|
80
87
|
Both sign-in tools answer "not approved yet" rather than failing while they wait, and calling them
|
|
81
88
|
again keeps waiting on the same sign-in. Neither replaces a token that is already stored unless asked
|
|
82
89
|
to with `replace`.
|
|
@@ -89,8 +96,9 @@ it as `subdomain`; say nothing and the name is generated, which is the right def
|
|
|
89
96
|
nobody is going to link to.
|
|
90
97
|
|
|
91
98
|
**A site can be documents instead of a built site.** A publish needs an `index.html` at the top level,
|
|
92
|
-
or at least one `.md`, `.markdown` or `.
|
|
93
|
-
note is a whole site, and Claude does not have to wrap it in HTML
|
|
99
|
+
or at least one document — `.md`, `.markdown`, `.pdf`, `.docx`, `.xlsx` or `.epub` — and those are
|
|
100
|
+
served through the reader. So a single note is a whole site, and Claude does not have to wrap it in HTML
|
|
101
|
+
to publish it.
|
|
94
102
|
|
|
95
103
|
**`publish_files` is text.** A PDF or an image has to come off a disk with `publish_dir`, because these
|
|
96
104
|
files arrive as JSON strings.
|
package/dist/index.js
CHANGED
|
@@ -8,59 +8,6 @@ import { createServer as createServer$1 } from "node:http";
|
|
|
8
8
|
import { realpath, readdir, stat, readFile } from "node:fs/promises";
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { z } from "zod";
|
|
11
|
-
async function call(credentials, path, init = {}) {
|
|
12
|
-
const response = await fetch(`${credentials.apiBaseUrl}/${path}`, {
|
|
13
|
-
...init,
|
|
14
|
-
headers: {
|
|
15
|
-
...init.headers,
|
|
16
|
-
Authorization: `Bearer ${credentials.token}`,
|
|
17
|
-
...init.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
if (!response.ok) {
|
|
21
|
-
const problem = await response.json().catch(() => null);
|
|
22
|
-
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
23
|
-
}
|
|
24
|
-
return await response.json();
|
|
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
|
-
}
|
|
46
|
-
async function listSites(credentials) {
|
|
47
|
-
const body = await call(credentials, "sites");
|
|
48
|
-
return body.sites;
|
|
49
|
-
}
|
|
50
|
-
async function createSite(credentials, subdomain) {
|
|
51
|
-
const created = await call(
|
|
52
|
-
credentials,
|
|
53
|
-
"sites",
|
|
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
|
-
}
|
|
61
|
-
);
|
|
62
|
-
return { ...created, name: null };
|
|
63
|
-
}
|
|
64
11
|
const TOKEN_VARIABLE = "DROP2RUN_TOKEN";
|
|
65
12
|
function configPath() {
|
|
66
13
|
return join(homedir(), ".config", "drop2run", "config.json");
|
|
@@ -137,6 +84,131 @@ function missingCredentialsMessage(surface = "cli") {
|
|
|
137
84
|
"The token is shown once when it is created and cannot be recovered afterwards."
|
|
138
85
|
].join("\n");
|
|
139
86
|
}
|
|
87
|
+
async function call(credentials, path, init = {}) {
|
|
88
|
+
const response = await fetch(`${credentials.apiBaseUrl}/${path}`, {
|
|
89
|
+
...init,
|
|
90
|
+
headers: {
|
|
91
|
+
...init.headers,
|
|
92
|
+
Authorization: `Bearer ${credentials.token}`,
|
|
93
|
+
...init.body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const problem = await response.json().catch(() => null);
|
|
98
|
+
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
99
|
+
}
|
|
100
|
+
return await response.json();
|
|
101
|
+
}
|
|
102
|
+
async function findSite(credentials, site) {
|
|
103
|
+
const found = await lookUpSite(credentials, site);
|
|
104
|
+
if (found === void 0) {
|
|
105
|
+
throw new Error(`No site of yours is called "${site}". \`drop2run ls\` shows what exists.`);
|
|
106
|
+
}
|
|
107
|
+
return found;
|
|
108
|
+
}
|
|
109
|
+
const SITE_ID_SHAPE = /^[0-9a-z]{26}$/i;
|
|
110
|
+
async function lookUpSite(credentials, site) {
|
|
111
|
+
const wanted = site.trim().toLowerCase();
|
|
112
|
+
if (wanted === "") return void 0;
|
|
113
|
+
const { sites } = await call(
|
|
114
|
+
credentials,
|
|
115
|
+
`sites?pageSize=${SITES_PER_REQUEST}&q=${encodeURIComponent(wanted)}`
|
|
116
|
+
);
|
|
117
|
+
const bySubdomain = sites.find((candidate) => candidate.subdomain.toLowerCase() === wanted);
|
|
118
|
+
if (bySubdomain !== void 0) return bySubdomain;
|
|
119
|
+
if (!SITE_ID_SHAPE.test(wanted)) return void 0;
|
|
120
|
+
const response = await fetch(
|
|
121
|
+
`${credentials.apiBaseUrl}/sites/${encodeURIComponent(wanted.toUpperCase())}`,
|
|
122
|
+
{ headers: { Authorization: `Bearer ${credentials.token}` } }
|
|
123
|
+
);
|
|
124
|
+
if (response.status === 404) return void 0;
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const problem = await response.json().catch(() => null);
|
|
127
|
+
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
128
|
+
}
|
|
129
|
+
const detail = await response.json();
|
|
130
|
+
return { siteId: detail.siteId, subdomain: detail.subdomain, url: detail.url, name: detail.name };
|
|
131
|
+
}
|
|
132
|
+
async function deleteSite(credentials, siteId) {
|
|
133
|
+
const response = await fetch(`${credentials.apiBaseUrl}/sites/${encodeURIComponent(siteId)}`, {
|
|
134
|
+
method: "DELETE",
|
|
135
|
+
headers: { Authorization: `Bearer ${credentials.token}` }
|
|
136
|
+
});
|
|
137
|
+
if (!response.ok) {
|
|
138
|
+
const problem = await response.json().catch(() => null);
|
|
139
|
+
throw new Error(problem?.detail ?? `The API answered ${response.status}.`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const SITES_PER_REQUEST = 100;
|
|
143
|
+
async function listSites(credentials) {
|
|
144
|
+
const body = await call(
|
|
145
|
+
credentials,
|
|
146
|
+
`sites?pageSize=${SITES_PER_REQUEST}`
|
|
147
|
+
);
|
|
148
|
+
return { sites: body.sites, total: body.total };
|
|
149
|
+
}
|
|
150
|
+
async function createSite(credentials, subdomain, folderId) {
|
|
151
|
+
const created = await call(
|
|
152
|
+
credentials,
|
|
153
|
+
"sites",
|
|
154
|
+
{
|
|
155
|
+
method: "POST",
|
|
156
|
+
// Absent fields rather than null ones, because the API reads null, empty and missing the same
|
|
157
|
+
// way and a body carrying only what was asked for is the one that reads correctly in a log.
|
|
158
|
+
body: JSON.stringify({
|
|
159
|
+
...subdomain === void 0 ? {} : { subdomain },
|
|
160
|
+
...folderId === void 0 ? {} : { folderId }
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
return { ...created, name: null };
|
|
165
|
+
}
|
|
166
|
+
async function listFolders(credentials) {
|
|
167
|
+
const body = await call(credentials, "site-folders");
|
|
168
|
+
const byId = new Map(body.folders.map((folder) => [folder.folderId, folder]));
|
|
169
|
+
const pathOf = (folderId) => {
|
|
170
|
+
const names = [];
|
|
171
|
+
let cursor = byId.get(folderId);
|
|
172
|
+
while (cursor !== void 0 && names.length <= byId.size) {
|
|
173
|
+
names.unshift(cursor.name);
|
|
174
|
+
cursor = cursor.parentId === null ? void 0 : byId.get(cursor.parentId);
|
|
175
|
+
}
|
|
176
|
+
return names.join("/");
|
|
177
|
+
};
|
|
178
|
+
return body.folders.map((folder) => ({ folderId: folder.folderId, path: pathOf(folder.folderId) })).sort((a, b) => a.path.localeCompare(b.path));
|
|
179
|
+
}
|
|
180
|
+
function comparablePath(path) {
|
|
181
|
+
return path.split("/").map((name) => name.trim()).filter((name) => name !== "").join("/").toLowerCase();
|
|
182
|
+
}
|
|
183
|
+
const FOLDERS_LISTED_ON_MISS = 20;
|
|
184
|
+
async function resolveFolder(credentials, folder) {
|
|
185
|
+
const wanted = comparablePath(folder);
|
|
186
|
+
if (wanted === "") {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`"${folder}" does not name a folder. Leave the folder out to put the site at the top level.`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const folders = await listFolders(credentials);
|
|
192
|
+
const byId = folders.find((candidate) => candidate.folderId.toLowerCase() === wanted);
|
|
193
|
+
if (byId !== void 0) return byId.folderId;
|
|
194
|
+
const matches = folders.filter((candidate) => comparablePath(candidate.path) === wanted);
|
|
195
|
+
if (matches.length === 1 && matches[0] !== void 0) return matches[0].folderId;
|
|
196
|
+
if (matches.length > 1) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`More than one folder is at "${folder}", because a folder name can contain "/". Name it by id instead: ${matches.map((match) => match.folderId).join(", ")}.`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (folders.length === 0) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
`No folder of yours is called "${folder}" — this account has no folders yet. Make one in the dashboard at ${dashboardUrlFor(credentials.apiBaseUrl)}, or leave the folder out.`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const listed = folders.slice(0, FOLDERS_LISTED_ON_MISS).map((candidate) => candidate.path);
|
|
207
|
+
const more = folders.length - listed.length;
|
|
208
|
+
throw new Error(
|
|
209
|
+
`No folder of yours is called "${folder}". Your folders: ${listed.join(", ")}${more > 0 ? `, and ${more} more` : ""}.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
140
212
|
function newAttempt() {
|
|
141
213
|
const verifier = randomBytes(32).toString("base64url");
|
|
142
214
|
return {
|
|
@@ -374,6 +446,485 @@ async function problemToError(response) {
|
|
|
374
446
|
const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
|
|
375
447
|
return new DeployError(code, message, problem);
|
|
376
448
|
}
|
|
449
|
+
const FILE_TYPES = [
|
|
450
|
+
// Documents. The three extensions that make a folder a documents site.
|
|
451
|
+
{
|
|
452
|
+
ext: ".md",
|
|
453
|
+
mime: "text/markdown; charset=utf-8",
|
|
454
|
+
kind: "markdown",
|
|
455
|
+
document: true,
|
|
456
|
+
reader: true,
|
|
457
|
+
editable: true
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
ext: ".markdown",
|
|
461
|
+
mime: "text/markdown; charset=utf-8",
|
|
462
|
+
kind: "markdown",
|
|
463
|
+
document: true,
|
|
464
|
+
reader: true,
|
|
465
|
+
editable: true
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
ext: ".pdf",
|
|
469
|
+
mime: "application/pdf",
|
|
470
|
+
kind: "pdf",
|
|
471
|
+
document: true,
|
|
472
|
+
reader: true,
|
|
473
|
+
editable: false
|
|
474
|
+
},
|
|
475
|
+
// Office documents and ebooks. Each has its own kind because each needs its own renderer, loaded
|
|
476
|
+
// only when one is opened — `.docx` through mammoth, `.xlsx` through read-excel-file, `.epub`
|
|
477
|
+
// through foliate-js.
|
|
478
|
+
{
|
|
479
|
+
ext: ".docx",
|
|
480
|
+
mime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
481
|
+
kind: "docx",
|
|
482
|
+
document: true,
|
|
483
|
+
reader: true,
|
|
484
|
+
editable: false
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
ext: ".xlsx",
|
|
488
|
+
mime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
489
|
+
kind: "xlsx",
|
|
490
|
+
document: true,
|
|
491
|
+
reader: true,
|
|
492
|
+
editable: false
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
ext: ".epub",
|
|
496
|
+
mime: "application/epub+zip",
|
|
497
|
+
kind: "epub",
|
|
498
|
+
document: true,
|
|
499
|
+
reader: true,
|
|
500
|
+
editable: false
|
|
501
|
+
},
|
|
502
|
+
// Prose the reader shows preformatted.
|
|
503
|
+
{
|
|
504
|
+
ext: ".txt",
|
|
505
|
+
mime: "text/plain; charset=utf-8",
|
|
506
|
+
kind: "text",
|
|
507
|
+
document: false,
|
|
508
|
+
reader: true,
|
|
509
|
+
editable: true
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
ext: ".log",
|
|
513
|
+
mime: "text/plain; charset=utf-8",
|
|
514
|
+
kind: "text",
|
|
515
|
+
document: false,
|
|
516
|
+
reader: true,
|
|
517
|
+
editable: false
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
ext: ".csv",
|
|
521
|
+
mime: "text/csv; charset=utf-8",
|
|
522
|
+
kind: "text",
|
|
523
|
+
document: false,
|
|
524
|
+
reader: true,
|
|
525
|
+
editable: true
|
|
526
|
+
},
|
|
527
|
+
// Structured text. Read through the reader inside a folder of notes, which is why these carry
|
|
528
|
+
// `reader: true` while the source formats below do not.
|
|
529
|
+
{
|
|
530
|
+
ext: ".json",
|
|
531
|
+
mime: "application/json",
|
|
532
|
+
kind: "code",
|
|
533
|
+
document: false,
|
|
534
|
+
reader: true,
|
|
535
|
+
editable: true
|
|
536
|
+
},
|
|
537
|
+
{
|
|
538
|
+
ext: ".yaml",
|
|
539
|
+
mime: "text/yaml; charset=utf-8",
|
|
540
|
+
kind: "code",
|
|
541
|
+
document: false,
|
|
542
|
+
reader: true,
|
|
543
|
+
editable: true
|
|
544
|
+
},
|
|
545
|
+
{
|
|
546
|
+
ext: ".yml",
|
|
547
|
+
mime: "text/yaml; charset=utf-8",
|
|
548
|
+
kind: "code",
|
|
549
|
+
document: false,
|
|
550
|
+
reader: true,
|
|
551
|
+
editable: true
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
// No registered media type, so it takes text/plain rather than an invented one. The point is
|
|
555
|
+
// only that a browser shows it instead of downloading it.
|
|
556
|
+
ext: ".toml",
|
|
557
|
+
mime: "text/plain; charset=utf-8",
|
|
558
|
+
kind: "code",
|
|
559
|
+
document: false,
|
|
560
|
+
reader: true,
|
|
561
|
+
editable: true
|
|
562
|
+
},
|
|
563
|
+
// Source the viewer highlights. A link straight to one of these hands over the file.
|
|
564
|
+
{
|
|
565
|
+
ext: ".css",
|
|
566
|
+
mime: "text/css; charset=utf-8",
|
|
567
|
+
kind: "code",
|
|
568
|
+
document: false,
|
|
569
|
+
reader: false,
|
|
570
|
+
editable: true
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
ext: ".js",
|
|
574
|
+
mime: "text/javascript; charset=utf-8",
|
|
575
|
+
kind: "code",
|
|
576
|
+
document: false,
|
|
577
|
+
reader: false,
|
|
578
|
+
editable: true
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
ext: ".ts",
|
|
582
|
+
mime: null,
|
|
583
|
+
kind: "code",
|
|
584
|
+
document: false,
|
|
585
|
+
reader: false,
|
|
586
|
+
editable: false
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
ext: ".tsx",
|
|
590
|
+
mime: null,
|
|
591
|
+
kind: "code",
|
|
592
|
+
document: false,
|
|
593
|
+
reader: false,
|
|
594
|
+
editable: false
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
ext: ".jsx",
|
|
598
|
+
mime: null,
|
|
599
|
+
kind: "code",
|
|
600
|
+
document: false,
|
|
601
|
+
reader: false,
|
|
602
|
+
editable: false
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
ext: ".sh",
|
|
606
|
+
mime: null,
|
|
607
|
+
kind: "code",
|
|
608
|
+
document: false,
|
|
609
|
+
reader: false,
|
|
610
|
+
editable: false
|
|
611
|
+
},
|
|
612
|
+
// The languages an assistant writes most, beyond the web's own. Same row as `.ts`: shown as source
|
|
613
|
+
// when somebody opens one in the reader or publishes it on its own, handed over as a file when a
|
|
614
|
+
// link names it directly, and no media type — adding one changes what a browser does with a
|
|
615
|
+
// published file, which I10 makes a decision of its own rather than a side effect of this list.
|
|
616
|
+
{
|
|
617
|
+
ext: ".py",
|
|
618
|
+
mime: null,
|
|
619
|
+
kind: "code",
|
|
620
|
+
document: false,
|
|
621
|
+
reader: false,
|
|
622
|
+
editable: false
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
ext: ".sql",
|
|
626
|
+
mime: null,
|
|
627
|
+
kind: "code",
|
|
628
|
+
document: false,
|
|
629
|
+
reader: false,
|
|
630
|
+
editable: false
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
ext: ".go",
|
|
634
|
+
mime: null,
|
|
635
|
+
kind: "code",
|
|
636
|
+
document: false,
|
|
637
|
+
reader: false,
|
|
638
|
+
editable: false
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
ext: ".rs",
|
|
642
|
+
mime: null,
|
|
643
|
+
kind: "code",
|
|
644
|
+
document: false,
|
|
645
|
+
reader: false,
|
|
646
|
+
editable: false
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
ext: ".java",
|
|
650
|
+
mime: null,
|
|
651
|
+
kind: "code",
|
|
652
|
+
document: false,
|
|
653
|
+
reader: false,
|
|
654
|
+
editable: false
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
ext: ".kt",
|
|
658
|
+
mime: null,
|
|
659
|
+
kind: "code",
|
|
660
|
+
document: false,
|
|
661
|
+
reader: false,
|
|
662
|
+
editable: false
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
ext: ".swift",
|
|
666
|
+
mime: null,
|
|
667
|
+
kind: "code",
|
|
668
|
+
document: false,
|
|
669
|
+
reader: false,
|
|
670
|
+
editable: false
|
|
671
|
+
},
|
|
672
|
+
{
|
|
673
|
+
ext: ".rb",
|
|
674
|
+
mime: null,
|
|
675
|
+
kind: "code",
|
|
676
|
+
document: false,
|
|
677
|
+
reader: false,
|
|
678
|
+
editable: false
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
ext: ".php",
|
|
682
|
+
mime: null,
|
|
683
|
+
kind: "code",
|
|
684
|
+
document: false,
|
|
685
|
+
reader: false,
|
|
686
|
+
editable: false
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
ext: ".c",
|
|
690
|
+
mime: null,
|
|
691
|
+
kind: "code",
|
|
692
|
+
document: false,
|
|
693
|
+
reader: false,
|
|
694
|
+
editable: false
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
ext: ".h",
|
|
698
|
+
mime: null,
|
|
699
|
+
kind: "code",
|
|
700
|
+
document: false,
|
|
701
|
+
reader: false,
|
|
702
|
+
editable: false
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
ext: ".cpp",
|
|
706
|
+
mime: null,
|
|
707
|
+
kind: "code",
|
|
708
|
+
document: false,
|
|
709
|
+
reader: false,
|
|
710
|
+
editable: false
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
ext: ".cs",
|
|
714
|
+
mime: null,
|
|
715
|
+
kind: "code",
|
|
716
|
+
document: false,
|
|
717
|
+
reader: false,
|
|
718
|
+
editable: false
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
ext: ".r",
|
|
722
|
+
mime: null,
|
|
723
|
+
kind: "code",
|
|
724
|
+
document: false,
|
|
725
|
+
reader: false,
|
|
726
|
+
editable: false
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
ext: ".xml",
|
|
730
|
+
mime: "application/xml",
|
|
731
|
+
kind: "code",
|
|
732
|
+
document: false,
|
|
733
|
+
reader: false,
|
|
734
|
+
editable: true
|
|
735
|
+
},
|
|
736
|
+
// Pictures, shown with an img element.
|
|
737
|
+
{
|
|
738
|
+
ext: ".png",
|
|
739
|
+
mime: "image/png",
|
|
740
|
+
kind: "image",
|
|
741
|
+
document: false,
|
|
742
|
+
reader: false,
|
|
743
|
+
editable: false
|
|
744
|
+
},
|
|
745
|
+
{
|
|
746
|
+
ext: ".jpg",
|
|
747
|
+
mime: "image/jpeg",
|
|
748
|
+
kind: "image",
|
|
749
|
+
document: false,
|
|
750
|
+
reader: false,
|
|
751
|
+
editable: false
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
ext: ".jpeg",
|
|
755
|
+
mime: "image/jpeg",
|
|
756
|
+
kind: "image",
|
|
757
|
+
document: false,
|
|
758
|
+
reader: false,
|
|
759
|
+
editable: false
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
ext: ".gif",
|
|
763
|
+
mime: "image/gif",
|
|
764
|
+
kind: "image",
|
|
765
|
+
document: false,
|
|
766
|
+
reader: false,
|
|
767
|
+
editable: false
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
ext: ".webp",
|
|
771
|
+
mime: "image/webp",
|
|
772
|
+
kind: "image",
|
|
773
|
+
document: false,
|
|
774
|
+
reader: false,
|
|
775
|
+
editable: false
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
ext: ".avif",
|
|
779
|
+
mime: "image/avif",
|
|
780
|
+
kind: "image",
|
|
781
|
+
document: false,
|
|
782
|
+
reader: false,
|
|
783
|
+
editable: false
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
// Text, hand-edited in practice, and script-carrying. Editable anyway: the Worker derives the
|
|
787
|
+
// type at serve time (I10) and already serves whatever was uploaded, so withholding the edit
|
|
788
|
+
// would withhold nothing else.
|
|
789
|
+
ext: ".svg",
|
|
790
|
+
mime: "image/svg+xml",
|
|
791
|
+
kind: "image",
|
|
792
|
+
document: false,
|
|
793
|
+
reader: false,
|
|
794
|
+
editable: true
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
ext: ".ico",
|
|
798
|
+
mime: "image/x-icon",
|
|
799
|
+
kind: "image",
|
|
800
|
+
document: false,
|
|
801
|
+
reader: false,
|
|
802
|
+
editable: false
|
|
803
|
+
},
|
|
804
|
+
// A customer's own pages. Never rendered inside the reader.
|
|
805
|
+
{
|
|
806
|
+
ext: ".html",
|
|
807
|
+
mime: "text/html; charset=utf-8",
|
|
808
|
+
kind: "html",
|
|
809
|
+
document: false,
|
|
810
|
+
reader: false,
|
|
811
|
+
editable: true
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
// ⚠️ No media type, and that is the state of the code rather than a decision: `TYPES` in
|
|
815
|
+
// `headers.ts` has never held `htm`, so a published `.htm` is served as octet-stream and
|
|
816
|
+
// downloads instead of rendering. Recorded here rather than fixed, because this table was
|
|
817
|
+
// introduced as a refactor and a refactor that changes what a URL answers is not one.
|
|
818
|
+
ext: ".htm",
|
|
819
|
+
mime: null,
|
|
820
|
+
kind: "html",
|
|
821
|
+
document: false,
|
|
822
|
+
reader: false,
|
|
823
|
+
editable: true
|
|
824
|
+
},
|
|
825
|
+
// Everything below renders as nothing — the viewer offers a download — but still needs a media
|
|
826
|
+
// type, because these are the files a static site is built out of.
|
|
827
|
+
{
|
|
828
|
+
ext: ".mjs",
|
|
829
|
+
mime: "text/javascript; charset=utf-8",
|
|
830
|
+
kind: "binary",
|
|
831
|
+
document: false,
|
|
832
|
+
reader: false,
|
|
833
|
+
editable: true
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
ext: ".cjs",
|
|
837
|
+
mime: null,
|
|
838
|
+
kind: "binary",
|
|
839
|
+
document: false,
|
|
840
|
+
reader: false,
|
|
841
|
+
editable: true
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
ext: ".map",
|
|
845
|
+
mime: "application/json",
|
|
846
|
+
kind: "binary",
|
|
847
|
+
document: false,
|
|
848
|
+
reader: false,
|
|
849
|
+
editable: false
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
ext: ".wasm",
|
|
853
|
+
mime: "application/wasm",
|
|
854
|
+
kind: "binary",
|
|
855
|
+
document: false,
|
|
856
|
+
reader: false,
|
|
857
|
+
editable: false
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
ext: ".woff2",
|
|
861
|
+
mime: "font/woff2",
|
|
862
|
+
kind: "binary",
|
|
863
|
+
document: false,
|
|
864
|
+
reader: false,
|
|
865
|
+
editable: false
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
ext: ".woff",
|
|
869
|
+
mime: "font/woff",
|
|
870
|
+
kind: "binary",
|
|
871
|
+
document: false,
|
|
872
|
+
reader: false,
|
|
873
|
+
editable: false
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
ext: ".ttf",
|
|
877
|
+
mime: "font/ttf",
|
|
878
|
+
kind: "binary",
|
|
879
|
+
document: false,
|
|
880
|
+
reader: false,
|
|
881
|
+
editable: false
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
ext: ".mp4",
|
|
885
|
+
mime: "video/mp4",
|
|
886
|
+
kind: "binary",
|
|
887
|
+
document: false,
|
|
888
|
+
reader: false,
|
|
889
|
+
editable: false
|
|
890
|
+
},
|
|
891
|
+
{
|
|
892
|
+
ext: ".webm",
|
|
893
|
+
mime: "video/webm",
|
|
894
|
+
kind: "binary",
|
|
895
|
+
document: false,
|
|
896
|
+
reader: false,
|
|
897
|
+
editable: false
|
|
898
|
+
}
|
|
899
|
+
];
|
|
900
|
+
function extensionsWhere(include) {
|
|
901
|
+
return FILE_TYPES.filter(include).map((type) => type.ext);
|
|
902
|
+
}
|
|
903
|
+
const DOCUMENT_EXTENSIONS$1 = extensionsWhere((type) => type.document);
|
|
904
|
+
const VIEWABLE_EXTENSIONS$1 = extensionsWhere(
|
|
905
|
+
(type) => type.kind !== "binary"
|
|
906
|
+
);
|
|
907
|
+
extensionsWhere((type) => type.reader);
|
|
908
|
+
extensionsWhere((type) => type.editable);
|
|
909
|
+
const WEB_ASSET_EXTENSIONS = [
|
|
910
|
+
".html",
|
|
911
|
+
".htm",
|
|
912
|
+
".js",
|
|
913
|
+
".mjs",
|
|
914
|
+
".cjs",
|
|
915
|
+
".css"
|
|
916
|
+
];
|
|
917
|
+
const GALLERY_EXTENSIONS = extensionsWhere(
|
|
918
|
+
(type) => type.kind === "image"
|
|
919
|
+
);
|
|
920
|
+
Object.fromEntries(
|
|
921
|
+
FILE_TYPES.filter((type) => type.mime !== null).map((type) => [type.ext, type.mime])
|
|
922
|
+
);
|
|
923
|
+
function extensionOf(path) {
|
|
924
|
+
const name = path.slice(path.lastIndexOf("/") + 1);
|
|
925
|
+
const dot = name.lastIndexOf(".");
|
|
926
|
+
return dot <= 0 ? "" : name.slice(dot).toLowerCase();
|
|
927
|
+
}
|
|
377
928
|
async function sha256Hex(bytes) {
|
|
378
929
|
const digest = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
|
|
379
930
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -405,52 +956,29 @@ function shouldIgnore(path) {
|
|
|
405
956
|
return segments.some((segment) => IGNORED_DIRECTORIES.includes(segment));
|
|
406
957
|
}
|
|
407
958
|
const REQUIRED_INDEX = "index.html";
|
|
408
|
-
const DOCUMENT_EXTENSIONS =
|
|
959
|
+
const DOCUMENT_EXTENSIONS = DOCUMENT_EXTENSIONS$1;
|
|
409
960
|
function isDocumentPath(path) {
|
|
410
961
|
const lower = path.toLowerCase();
|
|
411
962
|
return DOCUMENT_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
412
963
|
}
|
|
413
|
-
const VIEWABLE_EXTENSIONS =
|
|
414
|
-
// Documents — these also count at any depth, via DOCUMENT_EXTENSIONS.
|
|
415
|
-
".md",
|
|
416
|
-
".markdown",
|
|
417
|
-
".pdf",
|
|
418
|
-
// Prose the reader shows preformatted.
|
|
419
|
-
".txt",
|
|
420
|
-
".log",
|
|
421
|
-
".csv",
|
|
422
|
-
// Source the reader shows with highlighting.
|
|
423
|
-
".json",
|
|
424
|
-
".yaml",
|
|
425
|
-
".yml",
|
|
426
|
-
".toml",
|
|
427
|
-
".css",
|
|
428
|
-
".js",
|
|
429
|
-
".ts",
|
|
430
|
-
".tsx",
|
|
431
|
-
".jsx",
|
|
432
|
-
".sh",
|
|
433
|
-
".xml",
|
|
434
|
-
// Pictures the reader shows with an img element.
|
|
435
|
-
".png",
|
|
436
|
-
".jpg",
|
|
437
|
-
".jpeg",
|
|
438
|
-
".gif",
|
|
439
|
-
".webp",
|
|
440
|
-
".avif",
|
|
441
|
-
".svg",
|
|
442
|
-
".ico",
|
|
443
|
-
// See the note above: a floor for a caller that did not rename it.
|
|
444
|
-
".html",
|
|
445
|
-
".htm"
|
|
446
|
-
];
|
|
964
|
+
const VIEWABLE_EXTENSIONS = VIEWABLE_EXTENSIONS$1;
|
|
447
965
|
function isViewablePath(path) {
|
|
448
966
|
const lower = path.toLowerCase();
|
|
449
967
|
return VIEWABLE_EXTENSIONS.some((extension) => lower.endsWith(extension));
|
|
450
968
|
}
|
|
969
|
+
function isImageFolder(paths) {
|
|
970
|
+
let pictures = false;
|
|
971
|
+
for (const path of paths) {
|
|
972
|
+
const ext = extensionOf(path);
|
|
973
|
+
if (WEB_ASSET_EXTENSIONS.includes(ext)) return false;
|
|
974
|
+
if (GALLERY_EXTENSIONS.includes(ext)) pictures = true;
|
|
975
|
+
}
|
|
976
|
+
return pictures;
|
|
977
|
+
}
|
|
451
978
|
function canPublish(paths) {
|
|
452
979
|
if (paths.some((path) => path === REQUIRED_INDEX)) return true;
|
|
453
980
|
if (paths.some(isDocumentPath)) return true;
|
|
981
|
+
if (isImageFolder(paths)) return true;
|
|
454
982
|
return paths.length === 1 && paths[0] !== void 0 && isViewablePath(paths[0]);
|
|
455
983
|
}
|
|
456
984
|
function renameOfLoneHtmlPage(paths) {
|
|
@@ -479,37 +1007,42 @@ function checkLimits(files, limits) {
|
|
|
479
1007
|
`A site needs an ${REQUIRED_INDEX} at its top level, or at least one .md or .pdf file to publish as a documents site — or be a single file that can be displayed on its own, such as a note, a PDF, a page or an image. Drop the folder that contains it, not the folder above.`
|
|
480
1008
|
);
|
|
481
1009
|
}
|
|
482
|
-
|
|
1010
|
+
const refusal = sizeRefusal(
|
|
1011
|
+
files.map((file) => ({ path: file.path, size: file.bytes.length })),
|
|
1012
|
+
limits
|
|
1013
|
+
);
|
|
1014
|
+
if (refusal) throw refusal;
|
|
1015
|
+
}
|
|
1016
|
+
function sizeRefusal(files, limits) {
|
|
1017
|
+
if (!limits) return null;
|
|
483
1018
|
if (files.length > limits.maxFiles) {
|
|
484
|
-
|
|
1019
|
+
return new DeployError(
|
|
485
1020
|
ClientErrorCode.TooManyFiles,
|
|
486
1021
|
`That is ${files.length} files, and this plan allows ${limits.maxFiles}.`,
|
|
487
1022
|
{ limit: limits.maxFiles, actual: files.length }
|
|
488
1023
|
);
|
|
489
1024
|
}
|
|
490
|
-
const oversized = files.find((file) => file.
|
|
1025
|
+
const oversized = files.find((file) => file.size > limits.maxFileBytes);
|
|
491
1026
|
if (oversized) {
|
|
492
|
-
|
|
1027
|
+
return new DeployError(
|
|
493
1028
|
ClientErrorCode.FileTooLarge,
|
|
494
|
-
`${oversized.path} is ${formatBytes(oversized.
|
|
1029
|
+
`${oversized.path} is ${formatBytes(oversized.size)}, and this plan allows ${formatBytes(limits.maxFileBytes)} per file.`,
|
|
495
1030
|
{
|
|
496
1031
|
path: oversized.path,
|
|
497
1032
|
limit: limits.maxFileBytes,
|
|
498
|
-
actual: oversized.
|
|
1033
|
+
actual: oversized.size
|
|
499
1034
|
}
|
|
500
1035
|
);
|
|
501
1036
|
}
|
|
502
|
-
const total =
|
|
1037
|
+
const total = files.reduce((sum, file) => sum + file.size, 0);
|
|
503
1038
|
if (limits.maxSiteBytes !== null && total > limits.maxSiteBytes) {
|
|
504
|
-
|
|
1039
|
+
return new DeployError(
|
|
505
1040
|
ClientErrorCode.TooLarge,
|
|
506
1041
|
`That drop is ${formatBytes(total)}, and this plan allows ${formatBytes(limits.maxSiteBytes)}.`,
|
|
507
1042
|
{ limit: limits.maxSiteBytes, actual: total }
|
|
508
1043
|
);
|
|
509
1044
|
}
|
|
510
|
-
|
|
511
|
-
function totalBytes(files) {
|
|
512
|
-
return files.reduce((sum, file) => sum + file.bytes.length, 0);
|
|
1045
|
+
return null;
|
|
513
1046
|
}
|
|
514
1047
|
const MAX_NAME_LENGTH = 60;
|
|
515
1048
|
const HEAD_BYTES = 64 * 1024;
|
|
@@ -808,20 +1341,23 @@ function escapesRoot(root, candidate) {
|
|
|
808
1341
|
const inside = relative(root, candidate);
|
|
809
1342
|
return inside === "" || inside.startsWith("..");
|
|
810
1343
|
}
|
|
811
|
-
async function resolveSite(credentials, site,
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
}
|
|
818
|
-
return await createSite(credentials, subdomain);
|
|
1344
|
+
async function resolveSite(credentials, site, newSite) {
|
|
1345
|
+
const { subdomain, folder } = newSite;
|
|
1346
|
+
if (site !== void 0 && subdomain !== void 0) {
|
|
1347
|
+
throw new Error(
|
|
1348
|
+
"Give either an existing site or a subdomain for a new one, not both: publishing over a site and creating one are different requests."
|
|
1349
|
+
);
|
|
819
1350
|
}
|
|
820
|
-
if (site
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1351
|
+
if (site !== void 0 && folder !== void 0) {
|
|
1352
|
+
throw new Error(
|
|
1353
|
+
"A folder only says where a new site goes, so it cannot be given with an existing site. Leave the site out to create one in that folder, or move the existing site from the dashboard."
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
if (site === void 0) {
|
|
1357
|
+
const folderId = folder === void 0 ? void 0 : await resolveFolder(credentials, folder);
|
|
1358
|
+
return await createSite(credentials, subdomain, folderId);
|
|
1359
|
+
}
|
|
1360
|
+
const found = await lookUpSite(credentials, site);
|
|
825
1361
|
if (found === void 0) {
|
|
826
1362
|
throw new Error(
|
|
827
1363
|
`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.`
|
|
@@ -829,8 +1365,8 @@ async function resolveSite(credentials, site, subdomain) {
|
|
|
829
1365
|
}
|
|
830
1366
|
return found;
|
|
831
1367
|
}
|
|
832
|
-
async function publish(credentials, source, site, onProgress,
|
|
833
|
-
const target = await resolveSite(credentials, site,
|
|
1368
|
+
async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
1369
|
+
const target = await resolveSite(credentials, site, newSite);
|
|
834
1370
|
let files = 0;
|
|
835
1371
|
let url = target.url;
|
|
836
1372
|
let unchanged = false;
|
|
@@ -860,11 +1396,11 @@ async function publish(credentials, source, site, onProgress, subdomain) {
|
|
|
860
1396
|
});
|
|
861
1397
|
return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
|
|
862
1398
|
}
|
|
863
|
-
async function publishDirectory(credentials, directory, site, onProgress,
|
|
1399
|
+
async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
|
|
864
1400
|
const found = await stat(directory).catch(() => null);
|
|
865
1401
|
if (found === null) throw new Error(`There is no folder at ${directory}.`);
|
|
866
1402
|
if (!found.isDirectory()) throw new Error(`${directory} is a file, not a folder to publish.`);
|
|
867
|
-
return await publish(credentials, directorySource(directory), site, onProgress,
|
|
1403
|
+
return await publish(credentials, directorySource(directory), site, onProgress, newSite);
|
|
868
1404
|
}
|
|
869
1405
|
function cleanPath(path) {
|
|
870
1406
|
const cleaned = path.trim().replaceAll("\\", "/").replace(/^\.?\//, "");
|
|
@@ -878,7 +1414,7 @@ function cleanPath(path) {
|
|
|
878
1414
|
}
|
|
879
1415
|
return cleaned;
|
|
880
1416
|
}
|
|
881
|
-
async function publishFiles(credentials, files, site,
|
|
1417
|
+
async function publishFiles(credentials, files, site, newSite = {}) {
|
|
882
1418
|
const encoder = new TextEncoder();
|
|
883
1419
|
const collected = files.map((file) => ({
|
|
884
1420
|
path: cleanPath(file.path),
|
|
@@ -889,7 +1425,7 @@ async function publishFiles(credentials, files, site, subdomain) {
|
|
|
889
1425
|
if (seen.has(file.path)) throw new Error(`Two files were given the same path: ${file.path}`);
|
|
890
1426
|
seen.add(file.path);
|
|
891
1427
|
}
|
|
892
|
-
return publish(credentials, async () => collected, site, void 0,
|
|
1428
|
+
return publish(credentials, async () => collected, site, void 0, newSite);
|
|
893
1429
|
}
|
|
894
1430
|
const DEFAULT_WAIT_SECONDS = 120;
|
|
895
1431
|
const MAX_WAIT_SECONDS = 240;
|
|
@@ -1067,7 +1603,7 @@ async function signInWithCode(waitSeconds, replace = false) {
|
|
|
1067
1603
|
"Call this tool again to keep waiting. Nothing has been changed yet."
|
|
1068
1604
|
].join("\n");
|
|
1069
1605
|
}
|
|
1070
|
-
const SERVER_VERSION = "0.
|
|
1606
|
+
const SERVER_VERSION = "0.7.0";
|
|
1071
1607
|
const CREDENTIAL_WRITE = {
|
|
1072
1608
|
readOnlyHint: false,
|
|
1073
1609
|
destructiveHint: false,
|
|
@@ -1105,6 +1641,15 @@ const SITE_OUTPUT = z.object({
|
|
|
1105
1641
|
url: z.string().describe("Its live URL."),
|
|
1106
1642
|
name: z.string().nullable().describe("What it is called, or null when it has no name.")
|
|
1107
1643
|
});
|
|
1644
|
+
const FOLDER_OUTPUT = z.object({
|
|
1645
|
+
folderId: z.string().describe("Identifier of the folder, which `folder` accepts."),
|
|
1646
|
+
path: z.string().describe(
|
|
1647
|
+
"Every name from the top level down to it, joined with /, which `folder` accepts too."
|
|
1648
|
+
)
|
|
1649
|
+
});
|
|
1650
|
+
const FOLDER_INPUT = z.string().optional().describe(
|
|
1651
|
+
"Folder to file the new site in, when the person asked for one: its path of names such as Clients/Acme, or its id from list_folders. Only for a new site — do not pass it together with `site`; moving an existing site is done in the dashboard. It never creates a folder: a folder that does not exist is refused with the list of those that do, and nothing is published."
|
|
1652
|
+
);
|
|
1108
1653
|
function say(text, isError = false) {
|
|
1109
1654
|
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
|
|
1110
1655
|
}
|
|
@@ -1139,7 +1684,7 @@ function createServer() {
|
|
|
1139
1684
|
{ name: "drop2run", version: SERVER_VERSION },
|
|
1140
1685
|
{
|
|
1141
1686
|
instructions: [
|
|
1142
|
-
"Publishes static sites to Drop2Run.",
|
|
1687
|
+
"Publishes pages, documents and static sites to Drop2Run, and returns a URL.",
|
|
1143
1688
|
"",
|
|
1144
1689
|
"Use publish_files for anything you wrote here — a page, a markdown note, several files",
|
|
1145
1690
|
"together — and publish_dir for a folder that already exists on disk. Leave `site` unset to",
|
|
@@ -1152,9 +1697,14 @@ function createServer() {
|
|
|
1152
1697
|
"be deleted by hand. `site` and `subdomain` are opposites: one publishes over a site that",
|
|
1153
1698
|
"exists, the other creates one that does not. Never pass both.",
|
|
1154
1699
|
"",
|
|
1155
|
-
"A site
|
|
1156
|
-
"
|
|
1157
|
-
"
|
|
1700
|
+
"A new site goes at the top level of the dashboard unless `folder` files it somewhere. Pass",
|
|
1701
|
+
"it when the person named a folder, as its path (Clients/Acme) or its id; list_folders shows",
|
|
1702
|
+
"what exists. Like `subdomain`, it only describes a new site, so never pass it with `site`.",
|
|
1703
|
+
"",
|
|
1704
|
+
"A site needs an index.html at its top level, or at least one document (.md, .markdown,",
|
|
1705
|
+
".pdf, .docx, .xlsx or .epub), which publishes as a documents site and is read through a",
|
|
1706
|
+
"viewer. So one page goes at index.html, and a single note is a whole publish that needs",
|
|
1707
|
+
"no wrapping in HTML.",
|
|
1158
1708
|
"",
|
|
1159
1709
|
"Publishing needs an access token. If there is none, call login — it opens a browser and",
|
|
1160
1710
|
"stores one, and no command line or manual token is involved. Where no browser can be",
|
|
@@ -1212,13 +1762,14 @@ function createServer() {
|
|
|
1212
1762
|
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
|
|
1213
1763
|
subdomain: z.string().optional().describe(
|
|
1214
1764
|
"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."
|
|
1215
|
-
)
|
|
1765
|
+
),
|
|
1766
|
+
folder: FOLDER_INPUT
|
|
1216
1767
|
},
|
|
1217
1768
|
outputSchema: PUBLISH_OUTPUT,
|
|
1218
1769
|
annotations: REPLACES_A_SITE
|
|
1219
1770
|
},
|
|
1220
|
-
({ files, site, subdomain }) => withCredentials(async (credentials) => {
|
|
1221
|
-
const result = await publishFiles(credentials, files, site, subdomain);
|
|
1771
|
+
({ files, site, subdomain, folder }) => withCredentials(async (credentials) => {
|
|
1772
|
+
const result = await publishFiles(credentials, files, site, { subdomain, folder });
|
|
1222
1773
|
return report(describe(result), { ...result });
|
|
1223
1774
|
})
|
|
1224
1775
|
);
|
|
@@ -1226,19 +1777,23 @@ function createServer() {
|
|
|
1226
1777
|
"publish_dir",
|
|
1227
1778
|
{
|
|
1228
1779
|
title: "Publish a folder",
|
|
1229
|
-
description: "Publishes a folder
|
|
1780
|
+
description: "Publishes a folder from disk and returns its URL. The folder needs an index.html at its root, or documents (.md, .pdf, .docx, .xlsx, .epub), which are served through the reader.",
|
|
1230
1781
|
inputSchema: {
|
|
1231
1782
|
path: z.string().min(1).describe("Absolute path of the folder to publish."),
|
|
1232
1783
|
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
|
|
1233
1784
|
subdomain: z.string().optional().describe(
|
|
1234
1785
|
"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."
|
|
1235
|
-
)
|
|
1786
|
+
),
|
|
1787
|
+
folder: FOLDER_INPUT
|
|
1236
1788
|
},
|
|
1237
1789
|
outputSchema: PUBLISH_OUTPUT,
|
|
1238
1790
|
annotations: REPLACES_A_SITE
|
|
1239
1791
|
},
|
|
1240
|
-
({ path, site, subdomain }) => withCredentials(async (credentials) => {
|
|
1241
|
-
const result = await publishDirectory(credentials, path, site, void 0,
|
|
1792
|
+
({ path, site, subdomain, folder }) => withCredentials(async (credentials) => {
|
|
1793
|
+
const result = await publishDirectory(credentials, path, site, void 0, {
|
|
1794
|
+
subdomain,
|
|
1795
|
+
folder
|
|
1796
|
+
});
|
|
1242
1797
|
return report(describe(result), { ...result });
|
|
1243
1798
|
})
|
|
1244
1799
|
);
|
|
@@ -1279,15 +1834,36 @@ function createServer() {
|
|
|
1279
1834
|
title: "List sites",
|
|
1280
1835
|
description: "Lists the sites on this account, so a publish can go to one of them.",
|
|
1281
1836
|
inputSchema: {},
|
|
1282
|
-
outputSchema: {
|
|
1837
|
+
outputSchema: {
|
|
1838
|
+
sites: z.array(SITE_OUTPUT).describe("The account's sites, newest first — every one unless `total` is larger."),
|
|
1839
|
+
total: z.number().int().describe("How many sites the account holds. Larger than `sites` only past 100.")
|
|
1840
|
+
},
|
|
1283
1841
|
annotations: READS_ONLY
|
|
1284
1842
|
},
|
|
1285
1843
|
() => withCredentials(async (credentials) => {
|
|
1286
|
-
const sites = await listSites(credentials);
|
|
1844
|
+
const { sites, total } = await listSites(credentials);
|
|
1287
1845
|
const text = sites.length === 0 ? "No sites on this account yet. Publishing without a `site` creates one." : sites.map(
|
|
1288
1846
|
(site) => `${site.subdomain} — ${site.url}${site.name ? ` — ${site.name}` : ""}`
|
|
1289
|
-
).join("\n")
|
|
1290
|
-
|
|
1847
|
+
).join("\n") + (total > sites.length ? `
|
|
1848
|
+
… and ${total - sites.length} more; these are the newest ${sites.length} of ${total}.` : "");
|
|
1849
|
+
return report(text, { sites: sites.map((site) => ({ ...site })), total });
|
|
1850
|
+
})
|
|
1851
|
+
);
|
|
1852
|
+
server.registerTool(
|
|
1853
|
+
"list_folders",
|
|
1854
|
+
{
|
|
1855
|
+
title: "List folders",
|
|
1856
|
+
description: "Lists the folders on this account, each with the path a publish's `folder` accepts. Folders are made and rearranged in the dashboard; no tool here creates one.",
|
|
1857
|
+
inputSchema: {},
|
|
1858
|
+
outputSchema: {
|
|
1859
|
+
folders: z.array(FOLDER_OUTPUT).describe("Every folder on this account, ordered by path.")
|
|
1860
|
+
},
|
|
1861
|
+
annotations: READS_ONLY
|
|
1862
|
+
},
|
|
1863
|
+
() => withCredentials(async (credentials) => {
|
|
1864
|
+
const folders = await listFolders(credentials);
|
|
1865
|
+
const text = folders.length === 0 ? "No folders on this account. A new site goes at the top level; folders are made in the dashboard." : folders.map((folder) => `${folder.path} — ${folder.folderId}`).join("\n");
|
|
1866
|
+
return report(text, { folders: folders.map((folder) => ({ ...folder })) });
|
|
1291
1867
|
})
|
|
1292
1868
|
);
|
|
1293
1869
|
return server;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drop2run/mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server that publishes a
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "MCP server that publishes what an AI made — a page, a document or a whole site — to Drop2Run and returns a live URL.",
|
|
5
5
|
"mcpName": "run.dropto/drop2run",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|