@drop2run/mcp 0.11.0 → 0.12.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.
Files changed (2) hide show
  1. package/dist/index.js +282 -50
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -653,7 +653,9 @@ async function prepareDeploy(siteId, files, options, signal) {
653
653
  }));
654
654
  const prepared = await request(
655
655
  `sites/${encodeURIComponent(siteId)}/deploys/prepare`,
656
- { files: manifest },
656
+ // `batches` asks for batch permits; an API that predates them ignores the field, and one that
657
+ // knows them answers with single permits until they are switched on.
658
+ { files: manifest, batches: true },
657
659
  options,
658
660
  signal
659
661
  );
@@ -742,6 +744,47 @@ async function problemToError(response) {
742
744
  const message = typeof problem.detail === "string" ? problem.detail : typeof problem.title === "string" ? problem.title : `The server responded with ${response.status}.`;
743
745
  return new DeployError(code, message, problem);
744
746
  }
747
+ function batchLayout(members) {
748
+ return members.map((member) => `${member.sha256} ${member.size}
749
+ `).join("");
750
+ }
751
+ function isBatch(target) {
752
+ return "files" in target;
753
+ }
754
+ function parseBatchResult(body) {
755
+ try {
756
+ const parsed = JSON.parse(body);
757
+ if (parsed === null || typeof parsed !== "object") return null;
758
+ const { stored, present, failed } = parsed;
759
+ if (typeof stored !== "number" || typeof present !== "number" || !Array.isArray(failed))
760
+ return null;
761
+ return { stored, present, failed };
762
+ } catch {
763
+ return null;
764
+ }
765
+ }
766
+ async function mapInOrder(items, limit, fn, signal) {
767
+ const results = new Array(items.length);
768
+ let next = 0;
769
+ let failure = null;
770
+ const lane = async () => {
771
+ for (; ; ) {
772
+ if (failure !== null) return;
773
+ const index = next++;
774
+ if (index >= items.length) return;
775
+ try {
776
+ results[index] = await fn(items[index], index);
777
+ } catch (error) {
778
+ failure ??= { error };
779
+ return;
780
+ }
781
+ }
782
+ };
783
+ const lanes = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, lane);
784
+ await Promise.all(lanes);
785
+ if (failure !== null) throw failure.error;
786
+ return results;
787
+ }
745
788
  const FILE_TYPES = [
746
789
  // Documents. The three extensions that make a folder a documents site.
747
790
  {
@@ -1225,13 +1268,46 @@ async function sha256Hex(bytes) {
1225
1268
  const digest = await crypto.subtle.digest("SHA-256", bytes);
1226
1269
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
1227
1270
  }
1271
+ const HASH_CONCURRENCY = 4;
1272
+ const HASH_BYTES_IN_FLIGHT = 16 * 1024 * 1024;
1228
1273
  async function hashAll(files, onProgress, signal) {
1229
- const hashed = [];
1230
- for (const [index, file] of files.entries()) {
1231
- hashed.push({ ...file, sha256: await sha256Hex(await readForHash(file)) });
1232
- onProgress?.(index + 1, files.length);
1233
- }
1234
- return hashed;
1274
+ const budget = byteBudget(HASH_BYTES_IN_FLIGHT);
1275
+ let done = 0;
1276
+ return mapInOrder(
1277
+ files,
1278
+ HASH_CONCURRENCY,
1279
+ async (file) => {
1280
+ const size = sizeOf(file.bytes);
1281
+ await budget.acquire(size);
1282
+ try {
1283
+ signal?.throwIfAborted();
1284
+ const hashed = { ...file, sha256: await sha256Hex(await readForHash(file)) };
1285
+ done += 1;
1286
+ onProgress?.(done, files.length);
1287
+ return hashed;
1288
+ } finally {
1289
+ budget.release(size);
1290
+ }
1291
+ }
1292
+ );
1293
+ }
1294
+ function byteBudget(limit) {
1295
+ let held = 0;
1296
+ let waiting = [];
1297
+ return {
1298
+ async acquire(size) {
1299
+ while (held > 0 && held + size > limit) {
1300
+ await new Promise((resolve) => waiting.push(resolve));
1301
+ }
1302
+ held += size;
1303
+ },
1304
+ release(size) {
1305
+ held -= size;
1306
+ const woken = waiting;
1307
+ waiting = [];
1308
+ for (const wake of woken) wake();
1309
+ }
1310
+ };
1235
1311
  }
1236
1312
  async function readForHash(file) {
1237
1313
  try {
@@ -1260,10 +1336,13 @@ const IGNORED_DIRECTORIES = [
1260
1336
  ".codex",
1261
1337
  ".gemini"
1262
1338
  ];
1339
+ function isEnvironmentFile(name) {
1340
+ return name === ".env" || name === ".envrc" || name.startsWith(".env.");
1341
+ }
1263
1342
  function shouldIgnore(path) {
1264
1343
  const segments = path.split("/");
1265
1344
  const name = segments.at(-1) ?? "";
1266
- if (name === "" || IGNORED_EXACT.has(name)) return true;
1345
+ if (name === "" || IGNORED_EXACT.has(name) || isEnvironmentFile(name)) return true;
1267
1346
  if (name.startsWith("._")) return true;
1268
1347
  return segments.some((segment) => IGNORED_DIRECTORIES.includes(segment));
1269
1348
  }
@@ -1398,9 +1477,7 @@ async function readSiteName(files) {
1398
1477
  }
1399
1478
  }
1400
1479
  function decodeHead(bytes) {
1401
- return new TextDecoder("utf-8", { fatal: false }).decode(
1402
- bytes.subarray(0, HEAD_BYTES)
1403
- );
1480
+ return new TextDecoder("utf-8", { fatal: false }).decode(bytes.subarray(0, HEAD_BYTES));
1404
1481
  }
1405
1482
  function nameFromHtml(html) {
1406
1483
  const document = parse(html);
@@ -1425,9 +1502,7 @@ function parse(html) {
1425
1502
  }
1426
1503
  }
1427
1504
  function matchTag(html, tag) {
1428
- const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}\\s*>`, "i").exec(
1429
- html
1430
- );
1505
+ const match = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}\\s*>`, "i").exec(html);
1431
1506
  return match?.[1] === void 0 ? null : decodeEntities(match[1]);
1432
1507
  }
1433
1508
  function matchOgSiteName(html) {
@@ -1528,6 +1603,7 @@ function trimSuffix(title) {
1528
1603
  return title;
1529
1604
  }
1530
1605
  const DEFAULT_CONCURRENCY = 8;
1606
+ const MAX_BATCH_CONCURRENCY = 64;
1531
1607
  const DEFAULT_BYTES_IN_FLIGHT = 64 * 1024 * 1024;
1532
1608
  const DEFAULT_RETRIES = 3;
1533
1609
  const BACKOFF_MS = [1e3, 2e3, 4e3];
@@ -1544,14 +1620,31 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
1544
1620
  const put = deps.put ?? defaultTransport(deps.fetch);
1545
1621
  const delay = deps.delay ?? defaultDelay;
1546
1622
  const concurrency = deps.concurrency ?? DEFAULT_CONCURRENCY;
1623
+ const batchConcurrency = Math.max(
1624
+ 1,
1625
+ Math.min(MAX_BATCH_CONCURRENCY, Math.floor(deps.batchConcurrency ?? concurrency))
1626
+ );
1547
1627
  const retries = deps.retries ?? DEFAULT_RETRIES;
1548
1628
  const stallMs = deps.stallMs ?? DEFAULT_STALL_MS;
1549
1629
  const now = deps.now ?? Date.now;
1550
1630
  const maxBytesInFlight = deps.maxBytesInFlight ?? DEFAULT_BYTES_IN_FLIGHT;
1551
1631
  const byPath = new Map(files.map((file) => [file.path, file]));
1632
+ const fileCount = targets.reduce(
1633
+ (sum, target) => sum + (isBatch(target) ? target.files.length : 1),
1634
+ 0
1635
+ );
1552
1636
  let nextIndex = 0;
1553
1637
  let done = 0;
1554
1638
  let failed = false;
1639
+ let firstFailure;
1640
+ const local = new AbortController();
1641
+ const inner = local.signal;
1642
+ const fail = (error) => {
1643
+ if (failed) return;
1644
+ failed = true;
1645
+ firstFailure = error;
1646
+ local.abort();
1647
+ };
1555
1648
  let reserved = 0;
1556
1649
  let waiting = [];
1557
1650
  const acquire = async (size) => {
@@ -1566,17 +1659,32 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
1566
1659
  waiting = [];
1567
1660
  for (const wake of woken) wake();
1568
1661
  };
1662
+ const running = { single: 0, batch: 0 };
1663
+ const limit = { single: concurrency, batch: batchConcurrency };
1664
+ let slotWaiting = [];
1665
+ const takeSlot = async (kind) => {
1666
+ while (running[kind] >= limit[kind]) {
1667
+ await new Promise((resolve) => slotWaiting.push(resolve));
1668
+ }
1669
+ running[kind] += 1;
1670
+ };
1671
+ const freeSlot = (kind) => {
1672
+ running[kind] -= 1;
1673
+ const woken = slotWaiting;
1674
+ slotWaiting = [];
1675
+ for (const wake of woken) wake();
1676
+ };
1569
1677
  let landedBytes = 0;
1570
1678
  const inFlight = /* @__PURE__ */ new Map();
1571
1679
  let lastReport = Number.NEGATIVE_INFINITY;
1572
1680
  const report2 = (path) => {
1573
- if (onProgress === void 0) return;
1681
+ if (onProgress === void 0 || failed) return;
1574
1682
  const at = now();
1575
1683
  if (path === void 0 && at - lastReport < PROGRESS_INTERVAL_MS) return;
1576
1684
  lastReport = at;
1577
1685
  let bytes = landedBytes;
1578
1686
  for (const sent of inFlight.values()) bytes += sent;
1579
- onProgress(done, targets.length, bytes, path);
1687
+ onProgress(done, fileCount, bytes, path);
1580
1688
  };
1581
1689
  const worker = async () => {
1582
1690
  for (; ; ) {
@@ -1584,25 +1692,49 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
1584
1692
  const index = nextIndex++;
1585
1693
  const target = targets[index];
1586
1694
  if (target === void 0) return;
1587
- const file = byPath.get(target.path);
1588
- if (file === void 0) {
1589
- failed = true;
1590
- throw new DeployError(
1591
- ClientErrorCode.UploadFailed,
1592
- `The server asked for ${target.path}, which is not in the manifest.`,
1593
- { path: target.path }
1594
- );
1695
+ inner.throwIfAborted();
1696
+ const refs = isBatch(target) ? target.files : [target];
1697
+ const members = [];
1698
+ for (const ref of refs) {
1699
+ const file = byPath.get(ref.path);
1700
+ if (file === void 0) {
1701
+ const missing = new DeployError(
1702
+ ClientErrorCode.UploadFailed,
1703
+ `The server asked for ${ref.path}, which is not in the manifest.`,
1704
+ { path: ref.path }
1705
+ );
1706
+ fail(missing);
1707
+ throw missing;
1708
+ }
1709
+ members.push(file);
1595
1710
  }
1596
- const size = sizeOf(file.bytes);
1597
- await acquire(size);
1711
+ const layout = isBatch(target) ? new TextEncoder().encode(
1712
+ batchLayout(
1713
+ target.files.map((ref, i) => ({
1714
+ sha256: ref.sha256,
1715
+ size: sizeOf(members[i].bytes)
1716
+ }))
1717
+ )
1718
+ ) : null;
1719
+ const size = members.reduce((sum, file) => sum + sizeOf(file.bytes), layout?.length ?? 0);
1720
+ const copied = isBatch(target) ? members.reduce(
1721
+ (sum, file) => sum + (file.bytes instanceof Blob ? 0 : sizeOf(file.bytes)),
1722
+ 0
1723
+ ) : 0;
1724
+ const reservation = size + copied;
1725
+ const first = members[0].path;
1726
+ const label = members.length === 1 ? first : `${first} and ${members.length - 1} more`;
1727
+ const kind = isBatch(target) ? "batch" : "single";
1728
+ await takeSlot(kind);
1729
+ await acquire(reservation);
1598
1730
  try {
1599
1731
  if (failed) return;
1600
- signal?.throwIfAborted();
1601
- await uploadOne(target, file, {
1732
+ inner.throwIfAborted();
1733
+ const context = {
1602
1734
  put,
1603
1735
  delay,
1604
1736
  retries,
1605
- signal,
1737
+ signal: inner,
1606
1738
  uploadUrl,
1607
1739
  stallMs,
1608
1740
  onSent: (sent) => {
@@ -1611,26 +1743,34 @@ async function uploadAll(targets, uploadUrl, files, onProgress, signal, deps = {
1611
1743
  },
1612
1744
  onRetry: (attempt2, stalled) => {
1613
1745
  inFlight.set(index, 0);
1614
- onRetry?.(target.path, attempt2, retries + 1, stalled);
1746
+ onRetry?.(label, attempt2, retries + 1, stalled);
1615
1747
  }
1616
- });
1748
+ };
1749
+ if (isBatch(target) && layout !== null)
1750
+ await uploadBatchOne(target, members, layout, context);
1751
+ else await uploadOne(target, members[0], context);
1617
1752
  } catch (error) {
1618
- failed = true;
1753
+ fail(error);
1619
1754
  throw error;
1620
1755
  } finally {
1621
- release(size);
1756
+ release(reservation);
1757
+ freeSlot(kind);
1622
1758
  }
1623
1759
  inFlight.delete(index);
1624
- done += 1;
1760
+ done += members.length;
1625
1761
  landedBytes += size;
1626
- report2(file.path);
1762
+ report2(members.at(-1).path);
1627
1763
  }
1628
1764
  };
1629
- const workers = Array.from({ length: Math.min(concurrency, targets.length) }, worker);
1765
+ const workers = Array.from(
1766
+ { length: Math.min(Math.max(concurrency, batchConcurrency), targets.length) },
1767
+ worker
1768
+ );
1630
1769
  try {
1631
1770
  await Promise.all(workers);
1632
1771
  } catch (error) {
1633
- throw asDeployError(error);
1772
+ throw asDeployError(firstFailure ?? error);
1773
+ } finally {
1634
1774
  }
1635
1775
  }
1636
1776
  async function uploadOne(target, file, context) {
@@ -1643,7 +1783,7 @@ async function uploadOne(target, file, context) {
1643
1783
  context.signal?.throwIfAborted();
1644
1784
  }
1645
1785
  try {
1646
- const status = await context.put({
1786
+ const answer = await context.put({
1647
1787
  url: context.uploadUrl,
1648
1788
  token: target.token,
1649
1789
  body: untyped(file.bytes),
@@ -1651,6 +1791,7 @@ async function uploadOne(target, file, context) {
1651
1791
  onSent: context.onSent,
1652
1792
  stallMs: context.stallMs
1653
1793
  });
1794
+ const status = typeof answer === "number" ? answer : answer.status;
1654
1795
  if (status >= 200 && status < 300) return;
1655
1796
  if (status === 401 || status === 403 || status === 429) {
1656
1797
  throw new DeployError(
@@ -1679,6 +1820,85 @@ async function uploadOne(target, file, context) {
1679
1820
  cause: describeError(lastError)
1680
1821
  });
1681
1822
  }
1823
+ async function uploadBatchOne(batch, members, layout, context) {
1824
+ const first = members[0].path;
1825
+ const label = members.length === 1 ? first : `${first} and ${members.length - 1} more`;
1826
+ let lastError;
1827
+ for (let attempt2 = 0; attempt2 <= context.retries; attempt2++) {
1828
+ context.signal?.throwIfAborted();
1829
+ if (attempt2 > 0) {
1830
+ context.onRetry(attempt2 + 1, lastError instanceof StalledError);
1831
+ await context.delay(BACKOFF_MS[attempt2 - 1] ?? BACKOFF_MS.at(-1) ?? 1e3);
1832
+ context.signal?.throwIfAborted();
1833
+ }
1834
+ try {
1835
+ const body = new Blob([layout, ...members.map((file) => file.bytes)]);
1836
+ const answer = await context.put({
1837
+ url: context.uploadUrl,
1838
+ token: batch.token,
1839
+ body,
1840
+ signal: context.signal,
1841
+ onSent: context.onSent,
1842
+ stallMs: context.stallMs,
1843
+ wantsBody: true
1844
+ });
1845
+ const status = typeof answer === "number" ? answer : answer.status;
1846
+ if (status >= 200 && status < 300) {
1847
+ const result = typeof answer === "number" ? null : parseBatchResult(answer.body);
1848
+ if (result === null) {
1849
+ lastError = new Error(
1850
+ "The upload service answered with something that is not a batch result."
1851
+ );
1852
+ continue;
1853
+ }
1854
+ if (result.failed.length === 0) return;
1855
+ const changed = result.failed.find((failure) => failure.reason === "hash_mismatch");
1856
+ if (changed !== void 0) {
1857
+ const path = members[changed.i]?.path ?? first;
1858
+ throw new DeployError(
1859
+ ClientErrorCode.ReadFailed,
1860
+ `${path} changed after it was read, so it no longer matches what was checked. Drop the folder again.`,
1861
+ { path }
1862
+ );
1863
+ }
1864
+ lastError = new Error(
1865
+ `${result.failed.length} of ${members.length} files were not stored.`
1866
+ );
1867
+ continue;
1868
+ }
1869
+ if ([400, 401, 403, 411, 413, 429].includes(status)) {
1870
+ throw new DeployError(ClientErrorCode.UploadFailed, refusalMessage(status, label), {
1871
+ path: first,
1872
+ status
1873
+ });
1874
+ }
1875
+ lastError = new Error(`HTTP ${status}`);
1876
+ } catch (error) {
1877
+ if (error instanceof DeployError) throw error;
1878
+ if (isAbort(error)) throw cancelled();
1879
+ lastError = error;
1880
+ }
1881
+ }
1882
+ const message = lastError instanceof StalledError ? `Upload of ${label} stopped moving and did not recover after ${context.retries + 1} attempts. Check your connection and try again.` : `Could not upload ${label} after ${context.retries + 1} attempts.`;
1883
+ throw new DeployError(ClientErrorCode.UploadFailed, message, {
1884
+ path: first,
1885
+ cause: describeError(lastError)
1886
+ });
1887
+ }
1888
+ function refusalMessage(status, label) {
1889
+ switch (status) {
1890
+ case 401:
1891
+ return `Upload of ${label} was refused because its upload window expired. Try deploying again.`;
1892
+ case 429:
1893
+ return `Upload of ${label} was refused: this deploy has used every upload it was given. Try deploying again.`;
1894
+ case 400:
1895
+ case 411:
1896
+ case 413:
1897
+ return `Upload of ${label} was refused because it no longer matches what was checked — a file may have changed since you dropped it. Drop the folder again.`;
1898
+ default:
1899
+ return `Upload of ${label} was refused by the upload service. Try deploying again.`;
1900
+ }
1901
+ }
1682
1902
  function untyped(content) {
1683
1903
  if (ArrayBuffer.isView(content) || content.type === "") return content;
1684
1904
  return content.slice(0, content.size);
@@ -1689,7 +1909,8 @@ function defaultTransport(doFetch) {
1689
1909
  return Xhr === void 0 ? fetchTransport(globalFetch()) : xhrTransport(Xhr);
1690
1910
  }
1691
1911
  function fetchTransport(doFetch) {
1692
- return async ({ url, token, body, signal }) => {
1912
+ return async (request2) => {
1913
+ const { url, token, body, signal } = request2;
1693
1914
  const response = await doFetch(url, {
1694
1915
  method: "PUT",
1695
1916
  // The view, not a copy. Wrapping it in `new Blob([body.slice().buffer])` made two extra
@@ -1704,11 +1925,11 @@ function fetchTransport(doFetch) {
1704
1925
  headers: { Authorization: `Bearer ${token}` },
1705
1926
  ...signal ? { signal } : {}
1706
1927
  });
1707
- return response.status;
1928
+ return request2.wantsBody === true ? { status: response.status, body: await response.text() } : response.status;
1708
1929
  };
1709
1930
  }
1710
1931
  function xhrTransport(Xhr) {
1711
- return ({ url, token, body, signal, onSent, stallMs }) => new Promise((resolve, reject) => {
1932
+ return ({ url, token, body, signal, onSent, stallMs, wantsBody }) => new Promise((resolve, reject) => {
1712
1933
  const xhr = new Xhr();
1713
1934
  let settled = false;
1714
1935
  let timer;
@@ -1742,7 +1963,11 @@ function xhrTransport(Xhr) {
1742
1963
  onSent(event.loaded);
1743
1964
  };
1744
1965
  xhr.onprogress = arm;
1745
- xhr.onload = () => settle(() => resolve(xhr.status));
1966
+ xhr.onload = () => settle(
1967
+ () => resolve(
1968
+ wantsBody === true ? { status: xhr.status, body: xhr.responseText ?? "" } : xhr.status
1969
+ )
1970
+ );
1746
1971
  xhr.onerror = () => settle(() => reject(new TypeError("Network error during upload.")));
1747
1972
  arm();
1748
1973
  xhr.send(body);
@@ -1807,11 +2032,16 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
1807
2032
  return;
1808
2033
  }
1809
2034
  const sizes = new Map(hashed.map((file) => [file.path, sizeOf(file.bytes)]));
1810
- const totalBytes = prepared.upload.reduce(
1811
- (sum, target) => sum + (sizes.get(target.path) ?? 0),
1812
- 0
1813
- );
1814
- if (prepared.upload.length > 0) {
2035
+ const batches = prepared.batches ?? [];
2036
+ const targets = [...prepared.upload, ...batches];
2037
+ const totalBytes = prepared.upload.reduce((sum, target) => sum + (sizes.get(target.path) ?? 0), 0) + batches.reduce((sum, batch) => {
2038
+ const members = batch.files.map((ref) => ({
2039
+ sha256: ref.sha256,
2040
+ size: sizes.get(ref.path) ?? 0
2041
+ }));
2042
+ return sum + batchLayout(members).length + members.reduce((n, member) => n + member.size, 0);
2043
+ }, 0);
2044
+ if (targets.length > 0) {
1815
2045
  onProgress({
1816
2046
  type: "uploading",
1817
2047
  done: prepared.reused,
@@ -1822,7 +2052,7 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
1822
2052
  });
1823
2053
  }
1824
2054
  await uploadAll(
1825
- prepared.upload,
2055
+ targets,
1826
2056
  prepared.uploadUrl,
1827
2057
  hashed,
1828
2058
  (done, _total, bytes, path) => onProgress({
@@ -1835,7 +2065,9 @@ async function deploy(source, siteId, onProgress, signal, options = {}) {
1835
2065
  ...path === void 0 ? {} : { path }
1836
2066
  }),
1837
2067
  signal,
1838
- options.upload,
2068
+ // The server's number unless the caller set one: it is tuned on the server so a deploy of many
2069
+ // small files speeds up without a CLI or MCP release.
2070
+ typeof prepared.batchConcurrency === "number" && options.upload?.batchConcurrency === void 0 ? { ...options.upload, batchConcurrency: prepared.batchConcurrency } : options.upload,
1839
2071
  (path, attempt2, attempts, stalled) => onProgress({ type: "retrying", path, attempt: attempt2, attempts, stalled })
1840
2072
  );
1841
2073
  onProgress({ type: "completing", reused: prepared.reused });
@@ -2194,7 +2426,7 @@ async function signInWithCode(waitSeconds, replace = false) {
2194
2426
  "Call this tool again to keep waiting. Nothing has been changed yet."
2195
2427
  ].join("\n");
2196
2428
  }
2197
- const SERVER_VERSION = "0.11.0";
2429
+ const SERVER_VERSION = "0.12.0";
2198
2430
  const CREDENTIAL_WRITE = {
2199
2431
  readOnlyHint: false,
2200
2432
  destructiveHint: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drop2run/mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
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",