@drop2run/mcp 0.6.0 → 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 +6 -0
- package/dist/index.js +197 -83
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ lose it.
|
|
|
67
67
|
| `publish_files` | Publishes files Claude wrote — a page, a markdown note, several files together |
|
|
68
68
|
| `publish_dir` | Publishes a folder, given its absolute path |
|
|
69
69
|
| `list_sites` | Lists the sites on the account |
|
|
70
|
+
| `list_folders` | Lists the account's folders, each with the path `folder` accepts |
|
|
70
71
|
| `delete_site` | Deletes a site permanently, subdomain included — requires the subdomain repeated as `confirm` |
|
|
71
72
|
|
|
72
73
|
Both publish tools take an optional `site` — a subdomain or site id to publish over. Leave it out and a
|
|
@@ -78,6 +79,11 @@ afterwards, and a site made under a guessed one has to be deleted by hand. `site
|
|
|
78
79
|
opposites — one publishes over a site that exists, the other creates one that does not — so passing both
|
|
79
80
|
is refused rather than resolved.
|
|
80
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
|
+
|
|
81
87
|
Both sign-in tools answer "not approved yet" rather than failing while they wait, and calling them
|
|
82
88
|
again keeps waiting on the same sign-in. Neither replaces a token that is already stored unless asked
|
|
83
89
|
to with `replace`.
|
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 {
|
|
@@ -1269,20 +1341,23 @@ function escapesRoot(root, candidate) {
|
|
|
1269
1341
|
const inside = relative(root, candidate);
|
|
1270
1342
|
return inside === "" || inside.startsWith("..");
|
|
1271
1343
|
}
|
|
1272
|
-
async function resolveSite(credentials, site,
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
}
|
|
1279
|
-
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
|
+
);
|
|
1280
1350
|
}
|
|
1281
|
-
if (site
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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);
|
|
1286
1361
|
if (found === void 0) {
|
|
1287
1362
|
throw new Error(
|
|
1288
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.`
|
|
@@ -1290,8 +1365,8 @@ async function resolveSite(credentials, site, subdomain) {
|
|
|
1290
1365
|
}
|
|
1291
1366
|
return found;
|
|
1292
1367
|
}
|
|
1293
|
-
async function publish(credentials, source, site, onProgress,
|
|
1294
|
-
const target = await resolveSite(credentials, site,
|
|
1368
|
+
async function publish(credentials, source, site, onProgress, newSite = {}) {
|
|
1369
|
+
const target = await resolveSite(credentials, site, newSite);
|
|
1295
1370
|
let files = 0;
|
|
1296
1371
|
let url = target.url;
|
|
1297
1372
|
let unchanged = false;
|
|
@@ -1321,11 +1396,11 @@ async function publish(credentials, source, site, onProgress, subdomain) {
|
|
|
1321
1396
|
});
|
|
1322
1397
|
return { url, siteId: target.siteId, subdomain: target.subdomain, files, unchanged };
|
|
1323
1398
|
}
|
|
1324
|
-
async function publishDirectory(credentials, directory, site, onProgress,
|
|
1399
|
+
async function publishDirectory(credentials, directory, site, onProgress, newSite = {}) {
|
|
1325
1400
|
const found = await stat(directory).catch(() => null);
|
|
1326
1401
|
if (found === null) throw new Error(`There is no folder at ${directory}.`);
|
|
1327
1402
|
if (!found.isDirectory()) throw new Error(`${directory} is a file, not a folder to publish.`);
|
|
1328
|
-
return await publish(credentials, directorySource(directory), site, onProgress,
|
|
1403
|
+
return await publish(credentials, directorySource(directory), site, onProgress, newSite);
|
|
1329
1404
|
}
|
|
1330
1405
|
function cleanPath(path) {
|
|
1331
1406
|
const cleaned = path.trim().replaceAll("\\", "/").replace(/^\.?\//, "");
|
|
@@ -1339,7 +1414,7 @@ function cleanPath(path) {
|
|
|
1339
1414
|
}
|
|
1340
1415
|
return cleaned;
|
|
1341
1416
|
}
|
|
1342
|
-
async function publishFiles(credentials, files, site,
|
|
1417
|
+
async function publishFiles(credentials, files, site, newSite = {}) {
|
|
1343
1418
|
const encoder = new TextEncoder();
|
|
1344
1419
|
const collected = files.map((file) => ({
|
|
1345
1420
|
path: cleanPath(file.path),
|
|
@@ -1350,7 +1425,7 @@ async function publishFiles(credentials, files, site, subdomain) {
|
|
|
1350
1425
|
if (seen.has(file.path)) throw new Error(`Two files were given the same path: ${file.path}`);
|
|
1351
1426
|
seen.add(file.path);
|
|
1352
1427
|
}
|
|
1353
|
-
return publish(credentials, async () => collected, site, void 0,
|
|
1428
|
+
return publish(credentials, async () => collected, site, void 0, newSite);
|
|
1354
1429
|
}
|
|
1355
1430
|
const DEFAULT_WAIT_SECONDS = 120;
|
|
1356
1431
|
const MAX_WAIT_SECONDS = 240;
|
|
@@ -1528,7 +1603,7 @@ async function signInWithCode(waitSeconds, replace = false) {
|
|
|
1528
1603
|
"Call this tool again to keep waiting. Nothing has been changed yet."
|
|
1529
1604
|
].join("\n");
|
|
1530
1605
|
}
|
|
1531
|
-
const SERVER_VERSION = "0.
|
|
1606
|
+
const SERVER_VERSION = "0.7.0";
|
|
1532
1607
|
const CREDENTIAL_WRITE = {
|
|
1533
1608
|
readOnlyHint: false,
|
|
1534
1609
|
destructiveHint: false,
|
|
@@ -1566,6 +1641,15 @@ const SITE_OUTPUT = z.object({
|
|
|
1566
1641
|
url: z.string().describe("Its live URL."),
|
|
1567
1642
|
name: z.string().nullable().describe("What it is called, or null when it has no name.")
|
|
1568
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
|
+
);
|
|
1569
1653
|
function say(text, isError = false) {
|
|
1570
1654
|
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
|
|
1571
1655
|
}
|
|
@@ -1613,6 +1697,10 @@ function createServer() {
|
|
|
1613
1697
|
"be deleted by hand. `site` and `subdomain` are opposites: one publishes over a site that",
|
|
1614
1698
|
"exists, the other creates one that does not. Never pass both.",
|
|
1615
1699
|
"",
|
|
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
|
+
"",
|
|
1616
1704
|
"A site needs an index.html at its top level, or at least one document (.md, .markdown,",
|
|
1617
1705
|
".pdf, .docx, .xlsx or .epub), which publishes as a documents site and is read through a",
|
|
1618
1706
|
"viewer. So one page goes at index.html, and a single note is a whole publish that needs",
|
|
@@ -1674,13 +1762,14 @@ function createServer() {
|
|
|
1674
1762
|
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
|
|
1675
1763
|
subdomain: z.string().optional().describe(
|
|
1676
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."
|
|
1677
|
-
)
|
|
1765
|
+
),
|
|
1766
|
+
folder: FOLDER_INPUT
|
|
1678
1767
|
},
|
|
1679
1768
|
outputSchema: PUBLISH_OUTPUT,
|
|
1680
1769
|
annotations: REPLACES_A_SITE
|
|
1681
1770
|
},
|
|
1682
|
-
({ files, site, subdomain }) => withCredentials(async (credentials) => {
|
|
1683
|
-
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 });
|
|
1684
1773
|
return report(describe(result), { ...result });
|
|
1685
1774
|
})
|
|
1686
1775
|
);
|
|
@@ -1694,13 +1783,17 @@ function createServer() {
|
|
|
1694
1783
|
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
|
|
1695
1784
|
subdomain: z.string().optional().describe(
|
|
1696
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."
|
|
1697
|
-
)
|
|
1786
|
+
),
|
|
1787
|
+
folder: FOLDER_INPUT
|
|
1698
1788
|
},
|
|
1699
1789
|
outputSchema: PUBLISH_OUTPUT,
|
|
1700
1790
|
annotations: REPLACES_A_SITE
|
|
1701
1791
|
},
|
|
1702
|
-
({ path, site, subdomain }) => withCredentials(async (credentials) => {
|
|
1703
|
-
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
|
+
});
|
|
1704
1797
|
return report(describe(result), { ...result });
|
|
1705
1798
|
})
|
|
1706
1799
|
);
|
|
@@ -1741,15 +1834,36 @@ function createServer() {
|
|
|
1741
1834
|
title: "List sites",
|
|
1742
1835
|
description: "Lists the sites on this account, so a publish can go to one of them.",
|
|
1743
1836
|
inputSchema: {},
|
|
1744
|
-
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
|
+
},
|
|
1745
1841
|
annotations: READS_ONLY
|
|
1746
1842
|
},
|
|
1747
1843
|
() => withCredentials(async (credentials) => {
|
|
1748
|
-
const sites = await listSites(credentials);
|
|
1844
|
+
const { sites, total } = await listSites(credentials);
|
|
1749
1845
|
const text = sites.length === 0 ? "No sites on this account yet. Publishing without a `site` creates one." : sites.map(
|
|
1750
1846
|
(site) => `${site.subdomain} — ${site.url}${site.name ? ` — ${site.name}` : ""}`
|
|
1751
|
-
).join("\n")
|
|
1752
|
-
|
|
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 })) });
|
|
1753
1867
|
})
|
|
1754
1868
|
);
|
|
1755
1869
|
return server;
|
package/package.json
CHANGED