@drop2run/mcp 0.13.0 → 1.1.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 +30 -3
- package/dist/index.js +160 -25
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,32 @@
|
|
|
1
1
|
# @drop2run/mcp
|
|
2
2
|
|
|
3
|
-
An MCP server that lets any MCP client — Claude, Cursor, VS Code, Codex, Gemini CLI — publish what it made to
|
|
3
|
+
An MCP server that lets any MCP client — Claude, ChatGPT, Cursor, VS Code, Codex, Gemini CLI — publish what it made to
|
|
4
4
|
[Drop2Run](https://dropto.run): an HTML page, a markdown note, a folder of documents or a built site. It
|
|
5
5
|
returns a live HTTPS URL.
|
|
6
6
|
|
|
7
7
|
Full documentation at [dropto.run/docs/mcp](https://dropto.run/docs/mcp).
|
|
8
8
|
|
|
9
|
+
## In a chat on the web — no package at all
|
|
10
|
+
|
|
11
|
+
Claude on the web and on a phone, ChatGPT and other hosted chats cannot run this package, so the same
|
|
12
|
+
tools are hosted at a URL:
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
https://mcp.dropto.run/mcp
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Claude** — connect it from
|
|
19
|
+
[Claude's connector directory](https://claude.ai/directory/connectors/drop2run) and sign in to Drop2Run
|
|
20
|
+
when asked. To add it by URL instead, open
|
|
21
|
+
[this link](https://claude.ai/customize/connectors?modal=add-custom-connector&connectorName=Drop2Run&connectorUrl=https%3A%2F%2Fmcp.dropto.run%2Fmcp)
|
|
22
|
+
or *Settings → Connectors → Add custom connector* and paste the URL.
|
|
23
|
+
**Claude Code** — `claude mcp add --transport http drop2run https://mcp.dropto.run/mcp`. **ChatGPT** — add
|
|
24
|
+
it as a custom connector, where your plan offers developer mode.
|
|
25
|
+
|
|
26
|
+
The hosted server has no `login` / `login_code` (the chat signs in for it) and no `publish_dir` (there is
|
|
27
|
+
no disk), and `publish_files` takes text files only. For a folder on disk, a PDF or an image, run the
|
|
28
|
+
package below.
|
|
29
|
+
|
|
9
30
|
## Setup
|
|
10
31
|
|
|
11
32
|
**Claude Desktop** — download [`drop2run.mcpb`](https://dropto.run/drop2run.mcpb) and open it. Claude
|
|
@@ -87,6 +108,7 @@ lose it.
|
|
|
87
108
|
| `list_sites` | Lists the sites on the account |
|
|
88
109
|
| `list_folders` | Lists the account's folders, each with the path `folder` accepts |
|
|
89
110
|
| `delete_site` | Deletes a site permanently, subdomain included — requires the subdomain repeated as `confirm` |
|
|
111
|
+
| `change_subdomain` | Moves a site to a new subdomain; the old one keeps redirecting — requires the current subdomain as `confirm` |
|
|
90
112
|
| `get_site` | Reads a site's settings, whether it is paused, and its versions with the live one marked |
|
|
91
113
|
| `update_site` | Changes a site's name, serving mode, password, invite-only, comments, forms, scheduled takedown or folder |
|
|
92
114
|
| `list_people` | Lists who a site is shared with, what each may do, and whether only they can open it |
|
|
@@ -104,8 +126,8 @@ Both publish tools take an optional `site` — a subdomain or site id to publish
|
|
|
104
126
|
new site is created.
|
|
105
127
|
|
|
106
128
|
Both also take an optional `subdomain`, which names the new site instead of letting the server generate
|
|
107
|
-
one. It is for a person who asked for a particular address: the name becomes the URL,
|
|
108
|
-
|
|
129
|
+
one. It is for a person who asked for a particular address: the name becomes the URL, and a site made
|
|
130
|
+
under a guessed one has to be deleted, or moved with `change_subdomain`, by hand. `site` and `subdomain` are
|
|
109
131
|
opposites — one publishes over a site that exists, the other creates one that does not — so passing both
|
|
110
132
|
is refused rather than resolved.
|
|
111
133
|
|
|
@@ -114,6 +136,11 @@ by path such as `Clients/Acme`, or by the id `list_folders` shows. Like `subdoma
|
|
|
114
136
|
site, so it cannot be combined with `site`. It never creates a folder: one that does not exist is refused
|
|
115
137
|
with the list of those that do, and nothing is published.
|
|
116
138
|
|
|
139
|
+
`change_subdomain` moves a site to a new address. The old one keeps working by redirecting to the new
|
|
140
|
+
one, and nobody else can take it. Each site gets two changes, and a change back to an old name counts
|
|
141
|
+
too, so it asks for the current subdomain as `confirm`. Visitors who signed in to the site or typed its
|
|
142
|
+
password are asked again on the new address. `get_site` shows the changes left and the former names.
|
|
143
|
+
|
|
117
144
|
`update_site` changes only the settings it is given. An empty `name`, `password` or `expiresAt` clears
|
|
118
145
|
that setting, and `folder: "root"` moves the site back to the top level. A takedown pauses the site by
|
|
119
146
|
default; `expiryAction: "delete"` removes it instead, and asks for the subdomain as `confirm` the way
|
package/dist/index.js
CHANGED
|
@@ -87,11 +87,27 @@ function missingCredentialsMessage(surface = "cli") {
|
|
|
87
87
|
"The token is shown once when it is created and cannot be recovered afterwards."
|
|
88
88
|
].join("\n");
|
|
89
89
|
}
|
|
90
|
+
function problemCode(type, status) {
|
|
91
|
+
return typeof type === "string" && type !== "" && !/^[a-z][a-z0-9+.-]*:/i.test(type) ? type : `http_${status}`;
|
|
92
|
+
}
|
|
93
|
+
function problemTraceId(body) {
|
|
94
|
+
if (typeof body !== "object" || body === null) return null;
|
|
95
|
+
const value = body.traceId;
|
|
96
|
+
return typeof value === "string" && value !== "" ? value : null;
|
|
97
|
+
}
|
|
90
98
|
function sizeOf(content) {
|
|
91
|
-
|
|
99
|
+
if (ArrayBuffer.isView(content)) return content.byteLength;
|
|
100
|
+
if (isArrayBuffer(content)) return content.byteLength;
|
|
101
|
+
if (typeof content.size === "number") return content.size;
|
|
102
|
+
throw new TypeError("File contents must be a Uint8Array, an ArrayBuffer or a Blob.");
|
|
103
|
+
}
|
|
104
|
+
function isArrayBuffer(value) {
|
|
105
|
+
return Object.prototype.toString.call(value) === "[object ArrayBuffer]";
|
|
92
106
|
}
|
|
93
107
|
async function bytesOf(content) {
|
|
94
|
-
|
|
108
|
+
if (ArrayBuffer.isView(content)) return content;
|
|
109
|
+
if (isArrayBuffer(content)) return new Uint8Array(content);
|
|
110
|
+
return new Uint8Array(await content.arrayBuffer());
|
|
95
111
|
}
|
|
96
112
|
class DeployError extends Error {
|
|
97
113
|
/**
|
|
@@ -262,7 +278,7 @@ async function problemToError(response) {
|
|
|
262
278
|
`The server responded with ${response.status}.`
|
|
263
279
|
);
|
|
264
280
|
}
|
|
265
|
-
const code =
|
|
281
|
+
const code = problemCode(problem.type, response.status);
|
|
266
282
|
const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
|
|
267
283
|
return new DeployError(code, message, problem);
|
|
268
284
|
}
|
|
@@ -1534,6 +1550,7 @@ function asDeployError(error) {
|
|
|
1534
1550
|
);
|
|
1535
1551
|
}
|
|
1536
1552
|
async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
1553
|
+
let deployId;
|
|
1537
1554
|
try {
|
|
1538
1555
|
onProgress({ type: "reading", bytes: 0, total: 0 });
|
|
1539
1556
|
const collected = await source(
|
|
@@ -1555,6 +1572,7 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1555
1572
|
onProgress({ type: "unchanged", url: prepared.url ?? "" });
|
|
1556
1573
|
return;
|
|
1557
1574
|
}
|
|
1575
|
+
deployId = prepared.deployId;
|
|
1558
1576
|
const sizes = new Map(hashed.map((file) => [file.path, sizeOf(file.bytes)]));
|
|
1559
1577
|
const batches = prepared.batches ?? [];
|
|
1560
1578
|
const targets = [...prepared.upload, ...batches];
|
|
@@ -1615,7 +1633,8 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
|
|
|
1615
1633
|
type: "error",
|
|
1616
1634
|
code: failure.code,
|
|
1617
1635
|
message: failure.message,
|
|
1618
|
-
...failure.detail === void 0 ? {} : { detail: failure.detail }
|
|
1636
|
+
...failure.detail === void 0 ? {} : { detail: failure.detail },
|
|
1637
|
+
...deployId === void 0 ? {} : { deployId }
|
|
1619
1638
|
});
|
|
1620
1639
|
throw failure;
|
|
1621
1640
|
}
|
|
@@ -1636,7 +1655,9 @@ async function call(credentials, path, init = {}) {
|
|
|
1636
1655
|
async function failureOf(response) {
|
|
1637
1656
|
const problem = await response.json().catch(() => null);
|
|
1638
1657
|
return Object.assign(new Error(problem?.detail ?? `The API answered ${response.status}.`), {
|
|
1639
|
-
status: response.status
|
|
1658
|
+
status: response.status,
|
|
1659
|
+
code: problemCode(problem?.type, response.status),
|
|
1660
|
+
traceId: problemTraceId(problem)
|
|
1640
1661
|
});
|
|
1641
1662
|
}
|
|
1642
1663
|
function isTokenRefused(error) {
|
|
@@ -1654,6 +1675,7 @@ async function findSite(credentials, site) {
|
|
|
1654
1675
|
return found;
|
|
1655
1676
|
}
|
|
1656
1677
|
const SITE_ID_SHAPE = /^[0-9a-z]{26}$/i;
|
|
1678
|
+
const FORMER_NAME_CANDIDATES = 3;
|
|
1657
1679
|
async function lookUpSite(credentials, site) {
|
|
1658
1680
|
const wanted = site.trim().toLowerCase();
|
|
1659
1681
|
if (wanted === "") return void 0;
|
|
@@ -1663,6 +1685,13 @@ async function lookUpSite(credentials, site) {
|
|
|
1663
1685
|
);
|
|
1664
1686
|
const bySubdomain = sites.find((candidate) => candidate.subdomain.toLowerCase() === wanted);
|
|
1665
1687
|
if (bySubdomain !== void 0) return bySubdomain;
|
|
1688
|
+
for (const candidate of sites.slice(0, FORMER_NAME_CANDIDATES)) {
|
|
1689
|
+
const detail2 = await call(
|
|
1690
|
+
credentials,
|
|
1691
|
+
`sites/${encodeURIComponent(candidate.siteId)}`
|
|
1692
|
+
);
|
|
1693
|
+
if (detail2.formerSubdomains?.some((former) => former.subdomain === wanted)) return candidate;
|
|
1694
|
+
}
|
|
1666
1695
|
if (!SITE_ID_SHAPE.test(wanted)) return void 0;
|
|
1667
1696
|
const response = await fetch(
|
|
1668
1697
|
`${credentials.apiBaseUrl}/sites/${encodeURIComponent(wanted.toUpperCase())}`,
|
|
@@ -1746,7 +1775,19 @@ async function getSite(credentials, siteId) {
|
|
|
1746
1775
|
createdAt: deploy2.createdAt,
|
|
1747
1776
|
completedAt: deploy2.completedAt,
|
|
1748
1777
|
filesKept: deploy2.filesKept
|
|
1749
|
-
}))
|
|
1778
|
+
})),
|
|
1779
|
+
subdomainChangesLeft: site.subdomainChangesLeft ?? null,
|
|
1780
|
+
formerSubdomains: (site.formerSubdomains ?? []).map((former) => former.subdomain)
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
async function changeSubdomain(credentials, siteId, subdomain) {
|
|
1784
|
+
const changed = await call(credentials, `sites/${encodeURIComponent(siteId)}/subdomain`, {
|
|
1785
|
+
method: "POST",
|
|
1786
|
+
body: JSON.stringify({ subdomain })
|
|
1787
|
+
});
|
|
1788
|
+
return {
|
|
1789
|
+
...changed,
|
|
1790
|
+
formerSubdomains: changed.formerSubdomains.map((former) => former.subdomain)
|
|
1750
1791
|
};
|
|
1751
1792
|
}
|
|
1752
1793
|
async function updateSiteSettings(credentials, siteId, change) {
|
|
@@ -1938,6 +1979,10 @@ function describeMode(result) {
|
|
|
1938
1979
|
if (result.mode === "spa") return "Served as a single-page app.";
|
|
1939
1980
|
return "Served as a static site.";
|
|
1940
1981
|
}
|
|
1982
|
+
async function ownSiteAt(credentials, subdomain) {
|
|
1983
|
+
const found = await lookUpSite(credentials, subdomain);
|
|
1984
|
+
return found?.subdomain.toLowerCase() === subdomain.trim().toLowerCase() ? found : void 0;
|
|
1985
|
+
}
|
|
1941
1986
|
async function resolveSite(credentials, site, newSite) {
|
|
1942
1987
|
const { subdomain, folder } = newSite;
|
|
1943
1988
|
if (site !== void 0 && subdomain !== void 0) {
|
|
@@ -1952,6 +1997,13 @@ async function resolveSite(credentials, site, newSite) {
|
|
|
1952
1997
|
}
|
|
1953
1998
|
if (site === void 0) {
|
|
1954
1999
|
const folderId = folder === void 0 ? void 0 : await resolveFolder(credentials, folder);
|
|
2000
|
+
const held = subdomain === void 0 ? void 0 : await ownSiteAt(credentials, subdomain);
|
|
2001
|
+
if (held !== void 0) {
|
|
2002
|
+
if (held.liveDeployId === null) return held;
|
|
2003
|
+
throw new Error(
|
|
2004
|
+
`You already have a site at ${held.subdomain}, and it is live. To replace what it serves, publish to it as an existing site (the \`site\` argument, or \`--site\` on the command line); to make a new site, choose another subdomain.`
|
|
2005
|
+
);
|
|
2006
|
+
}
|
|
1955
2007
|
return await createSite(credentials, subdomain, folderId);
|
|
1956
2008
|
}
|
|
1957
2009
|
const found = await lookUpSite(credentials, site);
|
|
@@ -2234,6 +2286,12 @@ function accessOf(site) {
|
|
|
2234
2286
|
if (site.invitedOnly === false) return "public";
|
|
2235
2287
|
return "no password (whether it is invite-only could not be read)";
|
|
2236
2288
|
}
|
|
2289
|
+
function addressLines(site) {
|
|
2290
|
+
return [
|
|
2291
|
+
...site.formerSubdomains.length > 0 ? [` formerly ${site.formerSubdomains.join(", ")} (redirect here)`] : [],
|
|
2292
|
+
...site.subdomainChangesLeft === null ? [] : [` address ${site.subdomainChangesLeft} change(s) left`]
|
|
2293
|
+
];
|
|
2294
|
+
}
|
|
2237
2295
|
function describeSite(site, folderPath) {
|
|
2238
2296
|
const unavailable = (available) => available ? "" : " (not on this plan)";
|
|
2239
2297
|
const versions = site.deploys.slice(0, VERSIONS_LISTED).map((deploy2) => {
|
|
@@ -2253,12 +2311,29 @@ function describeSite(site, folderPath) {
|
|
|
2253
2311
|
` comments ${site.comments ?? "unknown"}${unavailable(site.commentsAvailable)}`,
|
|
2254
2312
|
` takedown ${site.expiresAt === null ? "none" : `${site.expiresAt} (${site.expiryAction})`}${unavailable(site.scheduledExpiryAvailable)}`,
|
|
2255
2313
|
` folder ${site.folderId === null ? "(top level)" : folderPath ?? site.folderId}`,
|
|
2314
|
+
...addressLines(site),
|
|
2256
2315
|
"",
|
|
2257
2316
|
site.deploys.length === 0 ? "No versions yet." : "Versions, newest first:",
|
|
2258
2317
|
...versions,
|
|
2259
2318
|
...more > 0 ? [` … and ${more} older`] : []
|
|
2260
2319
|
].join("\n");
|
|
2261
2320
|
}
|
|
2321
|
+
function failureContext(error) {
|
|
2322
|
+
if (typeof error !== "object" || error === null) return { code: null, traceId: null };
|
|
2323
|
+
const { code, traceId, detail } = error;
|
|
2324
|
+
return {
|
|
2325
|
+
code: typeof code === "string" && code !== "" ? code : null,
|
|
2326
|
+
traceId: typeof traceId === "string" && traceId !== "" ? traceId : problemTraceId(detail)
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
function reportLink(dashboardOrigin, context, source) {
|
|
2330
|
+
if (context.traceId === null) return null;
|
|
2331
|
+
const url = new URL("/report", dashboardOrigin);
|
|
2332
|
+
if (context.code !== null) url.searchParams.set("code", context.code);
|
|
2333
|
+
url.searchParams.set("trace", context.traceId);
|
|
2334
|
+
url.searchParams.set("source", source);
|
|
2335
|
+
return url.toString();
|
|
2336
|
+
}
|
|
2262
2337
|
const DEFAULT_WAIT_SECONDS = 120;
|
|
2263
2338
|
const MAX_WAIT_SECONDS = 240;
|
|
2264
2339
|
const ATTEMPT_LIFETIME_MS = 15 * 60 * 1e3;
|
|
@@ -2435,7 +2510,7 @@ async function signInWithCode(waitSeconds, replace = false) {
|
|
|
2435
2510
|
"Call this tool again to keep waiting. Nothing has been changed yet."
|
|
2436
2511
|
].join("\n");
|
|
2437
2512
|
}
|
|
2438
|
-
const SERVER_VERSION = "
|
|
2513
|
+
const SERVER_VERSION = "1.1.0";
|
|
2439
2514
|
const REPLACES_A_SITE = {
|
|
2440
2515
|
readOnlyHint: false,
|
|
2441
2516
|
destructiveHint: true,
|
|
@@ -2461,6 +2536,12 @@ const REMOVES_A_SITE = {
|
|
|
2461
2536
|
idempotentHint: false,
|
|
2462
2537
|
openWorldHint: true
|
|
2463
2538
|
};
|
|
2539
|
+
const MOVES_AN_ADDRESS = {
|
|
2540
|
+
readOnlyHint: false,
|
|
2541
|
+
destructiveHint: true,
|
|
2542
|
+
idempotentHint: true,
|
|
2543
|
+
openWorldHint: true
|
|
2544
|
+
};
|
|
2464
2545
|
const CHANGES_A_SITE = {
|
|
2465
2546
|
readOnlyHint: false,
|
|
2466
2547
|
destructiveHint: true,
|
|
@@ -2621,7 +2702,9 @@ function structuredSite(site) {
|
|
|
2621
2702
|
createdAt: deploy2.createdAt,
|
|
2622
2703
|
filesKept: deploy2.filesKept,
|
|
2623
2704
|
live: deploy2.deployId === site.liveDeployId
|
|
2624
|
-
}))
|
|
2705
|
+
})),
|
|
2706
|
+
subdomainChangesLeft: site.subdomainChangesLeft,
|
|
2707
|
+
formerSubdomains: [...site.formerSubdomains]
|
|
2625
2708
|
};
|
|
2626
2709
|
}
|
|
2627
2710
|
const SITE_OUTPUT = z.object({
|
|
@@ -2652,16 +2735,21 @@ async function withCredentials(context, work) {
|
|
|
2652
2735
|
() => work(credentials),
|
|
2653
2736
|
(error) => {
|
|
2654
2737
|
if (isTokenRefused(error)) context.tokenRefused?.();
|
|
2655
|
-
}
|
|
2738
|
+
},
|
|
2739
|
+
dashboardUrlFor(credentials.apiBaseUrl)
|
|
2656
2740
|
);
|
|
2657
2741
|
}
|
|
2658
|
-
async function attempt(work, observe) {
|
|
2742
|
+
async function attempt(work, observe, dashboardOrigin) {
|
|
2659
2743
|
try {
|
|
2660
2744
|
const answer = await work();
|
|
2661
2745
|
return typeof answer === "string" ? say(answer) : answer;
|
|
2662
2746
|
} catch (error) {
|
|
2663
2747
|
observe?.(error);
|
|
2664
|
-
|
|
2748
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2749
|
+
const report2 = dashboardOrigin === void 0 ? null : reportLink(dashboardOrigin, failureContext(error), "mcp");
|
|
2750
|
+
return say(report2 === null ? message : `${message}
|
|
2751
|
+
|
|
2752
|
+
Report it: ${report2}`, true);
|
|
2665
2753
|
}
|
|
2666
2754
|
}
|
|
2667
2755
|
function describe(result) {
|
|
@@ -2689,9 +2777,10 @@ function instructionsFor(surface) {
|
|
|
2689
2777
|
"",
|
|
2690
2778
|
"A new site gets a generated name unless `subdomain` asks for one. Pass it when the person",
|
|
2691
2779
|
"named the address they want, and otherwise leave it out — a name is what somebody will be",
|
|
2692
|
-
"given as a URL, it
|
|
2693
|
-
"be deleted by hand. `site` and `subdomain` are
|
|
2694
|
-
"exists, the other creates one that does not. Never
|
|
2780
|
+
"given as a URL, changing it later spends one of the two changes a site gets, and a site",
|
|
2781
|
+
"created under a guessed one has to be renamed or deleted by hand. `site` and `subdomain` are",
|
|
2782
|
+
"opposites: one publishes over a site that exists, the other creates one that does not. Never",
|
|
2783
|
+
"pass both.",
|
|
2695
2784
|
"",
|
|
2696
2785
|
"A new site goes at the top level of the dashboard unless `folder` files it somewhere. Pass",
|
|
2697
2786
|
"it when the person named a folder, as its path (Clients/Acme) or its id; list_folders shows",
|
|
@@ -2711,9 +2800,14 @@ function instructionsFor(surface) {
|
|
|
2711
2800
|
"Publishing over an existing site replaces what it serves, so ask before doing that to a",
|
|
2712
2801
|
"site the person did not name.",
|
|
2713
2802
|
"",
|
|
2714
|
-
"delete_site is permanent and
|
|
2715
|
-
"the subdomain and pass what they say as `confirm` — do not
|
|
2716
|
-
"know, because being told it is the point of the step.",
|
|
2803
|
+
"delete_site is permanent, and the subdomain stays reserved: nobody, the person included, can",
|
|
2804
|
+
"use it again. Ask the person for the subdomain and pass what they say as `confirm` — do not",
|
|
2805
|
+
"fill it in from what you already know, because being told it is the point of the step.",
|
|
2806
|
+
"",
|
|
2807
|
+
"change_subdomain moves a site to a new address. The old one keeps redirecting and nobody else",
|
|
2808
|
+
"can take it, but each site gets only a couple of changes, a change back to an old name",
|
|
2809
|
+
"included. Use it only when the person named the new address, and pass the site's current",
|
|
2810
|
+
"subdomain, as they say it, as `confirm`.",
|
|
2717
2811
|
"",
|
|
2718
2812
|
"An existing site's settings are read with get_site and changed with update_site: its name,",
|
|
2719
2813
|
"serving mode (static, spa, docs), a password, whether forms accept submissions, a scheduled",
|
|
@@ -2738,8 +2832,10 @@ function instructionsFor(surface) {
|
|
|
2738
2832
|
].join("\n");
|
|
2739
2833
|
}
|
|
2740
2834
|
function publishFilesDescription(surface) {
|
|
2741
|
-
|
|
2835
|
+
const body = surface === "local" ? "Publishes one or more files written here — markdown, HTML, CSS, JSON, source code, any text — as a site, and returns its URL. A single page goes at index.html; a single .md, .markdown or .pdf file is a site on its own, served through the documents viewer. Text only: a PDF or an image has to be published from disk with publish_dir." : "Publishes one or more files written here — markdown, HTML, CSS, JSON, source code, any text — as a site, and returns its URL. A single page goes at index.html; a single .md or .markdown file is a site on its own, served through the documents viewer. Text only: this connector cannot publish a PDF, an image or an Office file.";
|
|
2836
|
+
return `${body} ${RESERVE_FIRST}`;
|
|
2742
2837
|
}
|
|
2838
|
+
const RESERVE_FIRST = "To hold an address before the content exists — the person asks for the site first and the files later — publish a placeholder index.html with `subdomain`, then publish the real files over it with `site`. There is no way to create an empty site.";
|
|
2743
2839
|
function registerPublishFiles(server, context) {
|
|
2744
2840
|
server.registerTool(
|
|
2745
2841
|
"publish_files",
|
|
@@ -2755,7 +2851,7 @@ function registerPublishFiles(server, context) {
|
|
|
2755
2851
|
).min(1).describe("The files to publish."),
|
|
2756
2852
|
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
|
|
2757
2853
|
subdomain: z.string().optional().describe(
|
|
2758
|
-
"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
|
|
2854
|
+
"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, changing it later spends one of the few changes a site gets, and an unused site made under a guessed name has to be deleted by hand."
|
|
2759
2855
|
),
|
|
2760
2856
|
folder: FOLDER_INPUT
|
|
2761
2857
|
},
|
|
@@ -2772,7 +2868,7 @@ function registerSiteTools(server, context) {
|
|
|
2772
2868
|
"delete_site",
|
|
2773
2869
|
{
|
|
2774
2870
|
...titled("Delete a site", REMOVES_A_SITE),
|
|
2775
|
-
description: "Takes a site down permanently — its files, its history and its subdomain. Nothing here undoes it, and the subdomain
|
|
2871
|
+
description: "Takes a site down permanently — its files, its history and its subdomain. Nothing here undoes it, and the subdomain stays reserved — nobody can use it again. 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. To take a site off the air and keep it, use pause_site instead; resume_site brings it back.",
|
|
2776
2872
|
inputSchema: {
|
|
2777
2873
|
site: z.string().min(1).describe("Subdomain or site id to delete."),
|
|
2778
2874
|
confirm: z.string().min(1).describe(
|
|
@@ -2793,16 +2889,51 @@ function registerSiteTools(server, context) {
|
|
|
2793
2889
|
}
|
|
2794
2890
|
await deleteSite(credentials, found.siteId);
|
|
2795
2891
|
return report(
|
|
2796
|
-
`Deleted ${found.subdomain}. Its files and history are gone
|
|
2892
|
+
`Deleted ${found.subdomain}. Its files and history are gone; the subdomain stays reserved and cannot be used again.`,
|
|
2797
2893
|
{ siteId: found.siteId, subdomain: found.subdomain }
|
|
2798
2894
|
);
|
|
2799
2895
|
})
|
|
2800
2896
|
);
|
|
2897
|
+
server.registerTool(
|
|
2898
|
+
"change_subdomain",
|
|
2899
|
+
{
|
|
2900
|
+
...titled("Change a site's address", MOVES_AN_ADDRESS),
|
|
2901
|
+
description: "Moves a site to a new subdomain. The old one keeps working by redirecting to the new one, and nobody else can take it. Each site gets only a couple of changes, and a change back to an old name counts too, so use this only when the person named the new address. Requires `confirm` to repeat the site's current subdomain exactly; ask the person rather than filling it in. Visitors who signed in to the site or typed its password are asked again.",
|
|
2902
|
+
inputSchema: {
|
|
2903
|
+
site: SITE_INPUT,
|
|
2904
|
+
subdomain: z.string().min(1).describe("The new subdomain, as the person gave it."),
|
|
2905
|
+
confirm: z.string().min(1).describe(
|
|
2906
|
+
"The site's current subdomain, repeated exactly. A mismatch refuses the call rather than guessing which site was meant."
|
|
2907
|
+
)
|
|
2908
|
+
},
|
|
2909
|
+
outputSchema: {
|
|
2910
|
+
siteId: z.string().describe("Identifier of the site."),
|
|
2911
|
+
subdomain: z.string().describe("The subdomain it answers on now."),
|
|
2912
|
+
url: z.string().describe("Its address now."),
|
|
2913
|
+
subdomainChangesLeft: z.number().int().nullable().describe("How many more changes its plan allows, or null when it allows any number."),
|
|
2914
|
+
formerSubdomains: z.array(z.string()).describe("The subdomains it used to answer on, newest first; each redirects here.")
|
|
2915
|
+
}
|
|
2916
|
+
},
|
|
2917
|
+
({ site, subdomain, confirm }) => withCredentials(context, async (credentials) => {
|
|
2918
|
+
const found = await findSite(credentials, site);
|
|
2919
|
+
if (confirm.trim().toLowerCase() !== found.subdomain.toLowerCase()) {
|
|
2920
|
+
throw new Error(
|
|
2921
|
+
`Refusing to move ${found.subdomain}: \`confirm\` said "${confirm}". Repeat the current subdomain exactly to go ahead.`
|
|
2922
|
+
);
|
|
2923
|
+
}
|
|
2924
|
+
const changed = await changeSubdomain(credentials, found.siteId, subdomain);
|
|
2925
|
+
const left = changed.subdomainChangesLeft === null ? "" : ` ${changed.subdomainChangesLeft} address change(s) left.`;
|
|
2926
|
+
return report(
|
|
2927
|
+
`${found.subdomain} is now ${changed.url}. The old address redirects there.${left}`,
|
|
2928
|
+
{ ...changed, formerSubdomains: [...changed.formerSubdomains] }
|
|
2929
|
+
);
|
|
2930
|
+
})
|
|
2931
|
+
);
|
|
2801
2932
|
server.registerTool(
|
|
2802
2933
|
"list_sites",
|
|
2803
2934
|
{
|
|
2804
2935
|
...titled("List sites", READS_ONLY),
|
|
2805
|
-
description: "Lists the sites on this account, so a publish can go to one of them.",
|
|
2936
|
+
description: "Lists the sites on this account, newest first, so a publish can go to one of them. Returns up to 100 with the account's total; there are no pages to fetch beyond that. Use get_site for one site's settings and versions.",
|
|
2806
2937
|
inputSchema: {},
|
|
2807
2938
|
outputSchema: {
|
|
2808
2939
|
sites: z.array(SITE_OUTPUT).describe("The account's sites, newest first — every one unless `total` is larger."),
|
|
@@ -2851,7 +2982,11 @@ function registerSiteTools(server, context) {
|
|
|
2851
2982
|
scheduledTakedown: z.boolean(),
|
|
2852
2983
|
comments: z.boolean()
|
|
2853
2984
|
}).describe("Which settings the account's plan includes."),
|
|
2854
|
-
versions: z.array(VERSION_OUTPUT).describe("Its versions, newest first.")
|
|
2985
|
+
versions: z.array(VERSION_OUTPUT).describe("Its versions, newest first."),
|
|
2986
|
+
subdomainChangesLeft: z.number().int().nullable().describe(
|
|
2987
|
+
"How many more times change_subdomain may move it, or null when there is no limit."
|
|
2988
|
+
),
|
|
2989
|
+
formerSubdomains: z.array(z.string()).describe("The subdomains it used to answer on, newest first; each redirects to it.")
|
|
2855
2990
|
}
|
|
2856
2991
|
},
|
|
2857
2992
|
({ site }) => withCredentials(context, async (credentials) => {
|
|
@@ -3229,7 +3364,7 @@ function createServer() {
|
|
|
3229
3364
|
"login",
|
|
3230
3365
|
{
|
|
3231
3366
|
...titled("Sign in to Drop2Run", CREDENTIAL_WRITE),
|
|
3232
|
-
description: "Signs in through a browser on this machine and stores an access token, which every other tool then uses. Opens the browser here and waits for the person to approve. If it answers that nothing has been approved yet, call it again to keep waiting — that is not a failure. Needs no command line and no token pasted by hand.",
|
|
3367
|
+
description: "Signs in through a browser on this machine and stores an access token, which every other tool then uses. Opens the browser here and waits for the person to approve. If it answers that nothing has been approved yet, call it again to keep waiting — that is not a failure. Needs no command line and no token pasted by hand. Where no browser can open on this machine — a container, a remote host — use login_code instead.",
|
|
3233
3368
|
inputSchema: {
|
|
3234
3369
|
waitSeconds: z.number().int().min(5).max(MAX_WAIT_SECONDS).optional().describe("How long this call waits for the approval before answering. Default 120."),
|
|
3235
3370
|
replace: z.boolean().optional().describe(
|
|
@@ -3261,7 +3396,7 @@ function createServer() {
|
|
|
3261
3396
|
path: z.string().min(1).describe("Absolute path of the folder to publish."),
|
|
3262
3397
|
site: z.string().optional().describe("Subdomain or site id to publish over. Omit to create a new site."),
|
|
3263
3398
|
subdomain: z.string().optional().describe(
|
|
3264
|
-
"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
|
|
3399
|
+
"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, changing it later spends one of the few changes a site gets, and an unused site made under a guessed name has to be deleted by hand."
|
|
3265
3400
|
),
|
|
3266
3401
|
folder: FOLDER_INPUT
|
|
3267
3402
|
},
|
package/package.json
CHANGED