@appsoftwareltd/etherpk-mcp 0.10.0 → 0.10.2

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 CHANGED
@@ -38,8 +38,10 @@ No sign-in and no Sync Server:
38
38
  claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --folder /path/to/your/notes
39
39
  ```
40
40
 
41
- The folder must already be an EtherPK graph, so open it in EtherPK once. **Settings → Agents**
42
- shows this command with the path filled in once the folder's path is set on the General tab.
41
+ The folder must already be an EtherPK graph, with `pages/` and `journals/` folders in it. EtherPK
42
+ makes them when it opens a folder, and the [agent skill](#the-agent-skill) makes them for a new
43
+ one. **Settings → Agents** shows this command with the path filled in once the folder's path is set
44
+ on the General tab.
43
45
 
44
46
  ### Ask about your notes
45
47
 
@@ -169,9 +171,13 @@ npx @appsoftwareltd/etherpk-mcp publish --graph <id or name> --publication <id>
169
171
 
170
172
  - `--out` is remembered for that graph and publication. Later runs without it, and the agent's
171
173
  `publish` tool, write to the same folder.
172
- - The top-level `.html` files, `assets/`, `theme/` and the search, feed and report files are
173
- rewritten, and files that no longer belong are removed. Everything else in the folder is left
174
- alone.
174
+ - The top-level `.html` files, `assets/`, `theme/` and the search and feed files are rewritten,
175
+ and files that no longer belong are removed. Everything else in the folder is left alone.
176
+ - The folder must be one of its own, outside the graph's folder. A publish refuses the graph's
177
+ folder, a folder inside it and a folder that holds it, because it would remove the graph's files.
178
+ - `--dry-run` in place of `--out` renders and checks the publication and prints the whole report,
179
+ with the names of the pages it mentions, without writing anything. It needs no folder. Keep its
180
+ output out of public logs. The agent's `publish` tool takes `dryRun: true` for the same check.
175
181
  - Deploying the folder is up to you. Run the command from cron for a site that republishes itself.
176
182
  - Mermaid diagrams and maps need a browser. A map is published as a picture of the map, drawn over
177
183
  EtherPK's map host unless `ETHERPK_MAP_STYLE_URL` names another style. `diagrams setup` installs a
@@ -180,6 +186,17 @@ npx @appsoftwareltd/etherpk-mcp publish --graph <id or name> --publication <id>
180
186
  computer instead, which NixOS needs.
181
187
  - `list_publications`, or **Settings → Publish** in EtherPK, shows the publication ids.
182
188
 
189
+ ## The agent skill
190
+
191
+ The `etherpk` agent skill teaches an agent to make a blog, docs site or wiki folder in EtherPK's
192
+ format inside any project, to check it with `publish --dry-run`, and to publish it with this
193
+ command. For a synced graph it sends the agent to the tools above. Install it in the project the
194
+ agent works in, or with `-g` for every project:
195
+
196
+ ```sh
197
+ npx skills add appsoftwareltd/etherpk-client
198
+ ```
199
+
183
200
  ## Reference
184
201
 
185
202
  Every command is `npx @appsoftwareltd/etherpk-mcp <command>`. For the short form, `etherpk-mcp`,
package/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { availableParallelism, homedir, hostname, tmpdir } from "node:os";
4
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import nodePath, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { parseArgs } from "node:util";
6
6
  import { access, chmod, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
7
7
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
@@ -717,7 +717,7 @@ function looksLikeAgentSetupCode(value) {
717
717
  }
718
718
  var package_default = {
719
719
  name: "@appsoftwareltd/etherpk-mcp",
720
- version: "0.10.0",
720
+ version: "0.10.2",
721
721
  license: "Elastic-2.0",
722
722
  description: "EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.",
723
723
  type: "module",
@@ -14847,6 +14847,45 @@ async function writePublishFolders(path, folders) {
14847
14847
  function publishFolderOf(folders, graphKey, publicationId) {
14848
14848
  return folders.folders[graphKey]?.[publicationId] ?? null;
14849
14849
  }
14850
+ /**
14851
+ * How two absolute folder paths relate, by the platform's own `relative` rather than by splitting
14852
+ * on `/` (Windows paths use `\`, and a shared prefix such as `notes` and `notes-site` is not a
14853
+ * parent). Spelling only: {@link publishFolderOverlap} resolves symbolic links first.
14854
+ */
14855
+ function relateFolders(graphFolder, publishFolder, options = {}) {
14856
+ const path = options.path ?? nodePath;
14857
+ const caseInsensitive = options.caseInsensitive ?? (process.platform === "darwin" || process.platform === "win32");
14858
+ const fold = (p) => caseInsensitive ? path.resolve(p).toLowerCase() : path.resolve(p);
14859
+ const graph = fold(graphFolder);
14860
+ const site = fold(publishFolder);
14861
+ const below = (from, to) => {
14862
+ const rel = path.relative(from, to);
14863
+ return rel !== "" && rel !== ".." && !rel.startsWith(`..${path.sep}`) && !path.isAbsolute(rel);
14864
+ };
14865
+ if (path.relative(graph, site) === "") return "same";
14866
+ if (below(graph, site)) return "inside";
14867
+ if (below(site, graph)) return "contains";
14868
+ return null;
14869
+ }
14870
+ /**
14871
+ * A path with its symbolic links resolved, for a folder that may not exist yet: the nearest
14872
+ * existing ancestor is resolved and the rest appended, so `--out ./site` under a linked parent is
14873
+ * compared where it will really be written.
14874
+ */
14875
+ async function canonicalFolder(folder) {
14876
+ const absolute = resolve(folder);
14877
+ try {
14878
+ return await realpath(absolute);
14879
+ } catch {
14880
+ const parent = dirname(absolute);
14881
+ if (parent === absolute) return absolute;
14882
+ return join(await canonicalFolder(parent), basename(absolute));
14883
+ }
14884
+ }
14885
+ /** {@link relateFolders} over the folders as they are on disk, symbolic links followed. */
14886
+ async function publishFolderOverlap(graphFolder, publishFolder) {
14887
+ return relateFolders(await canonicalFolder(graphFolder), await canonicalFolder(publishFolder));
14888
+ }
14850
14889
  /** The same file with one publication's folder set (made absolute) - the caller writes it. */
14851
14890
  function withPublishFolder(folders, graphKey, publicationId, folder) {
14852
14891
  return { folders: {
@@ -17579,6 +17618,26 @@ async function findPublication(graph, id, host) {
17579
17618
  if (!found) throw publicationNotFound(id, publications, via);
17580
17619
  return found;
17581
17620
  }
17621
+ var OVERLAP_WORDS = {
17622
+ same: "is the folder of",
17623
+ inside: "is inside the folder of",
17624
+ contains: "holds the folder of"
17625
+ };
17626
+ /**
17627
+ * Refuse a Publish Folder that overlaps a served folder: the graph's own folder, one inside it, or
17628
+ * one holding it. A publish deletes the files it did not write from the folder it writes into
17629
+ * (`isOwnedPath`), which would take the graph's attachments, or mix the site's files into the
17630
+ * graph. Checked where a folder is set (`--out`) and again before every publish, since the
17631
+ * folder may have been set before this check existed. A synced graph has no folder here.
17632
+ */
17633
+ async function checkPublishFolder(graph, publicationId, folder, host) {
17634
+ if (graph.backend.kind !== "folder") return;
17635
+ const overlap = await publishFolderOverlap(graph.backend.path, folder);
17636
+ if (!overlap) return;
17637
+ const { cmd, via } = hostOf$1(host);
17638
+ const command = `${cmd} publish --folder "${graph.backend.path}" --publication ${publicationId} --out <a folder outside the graph>`;
17639
+ throw new ToolError("publish_folder_overlaps_graph", `${folder} ${OVERLAP_WORDS[overlap]} the graph "${graph.name}" (${graph.backend.path}). A publish deletes the files it did not write from the folder it publishes into, so the site needs a folder of its own outside the graph. Nothing was published. ` + (via === "cli" ? `Run: ${command}` : `Ask the user to run: ${command}`));
17640
+ }
17582
17641
  /** The publication page's mapping and body, through the raw handles: what `publish-service` writes to. */
17583
17642
  function frontmatterStore(graph) {
17584
17643
  return {
@@ -17710,7 +17769,7 @@ function head(items, n = 20) {
17710
17769
  /**
17711
17770
  * Publish one publication into its Publish Folder and report. The folder is the one a person
17712
17771
  * set for this graph and publication on this machine (ADR 0086); a publish with Mermaid needs
17713
- * the browser (ADR 0084).
17772
+ * the browser (ADR 0084). A dry run does everything but write, so it needs no folder.
17714
17773
  *
17715
17774
  * The result IS the report, trimmed to counts and first entries: nothing of it is written into
17716
17775
  * the folder, because a static host serves whatever the folder holds and the report names every
@@ -17727,10 +17786,12 @@ async function publish(graph, args, host) {
17727
17786
  const publication = summary.publications.find((p) => p.id === args.id.trim());
17728
17787
  if (!publication) throw publicationNotFound(args.id, summary.publications, via);
17729
17788
  const folder = (await foldersFor(graph, env))(publication.id);
17730
- if (!folder) {
17789
+ const dryRun = args.dryRun === true;
17790
+ if (!folder && !dryRun) {
17731
17791
  const where = graph.backend.kind === "folder" ? `--folder "${graph.backend.path}"` : `--graph "${graph.name}"`;
17732
17792
  throw new ToolError("no_publish_folder", `No publish folder is set for "${publication.name}" on this machine. Ask the user to run: ${cmd} publish ${where} --publication ${publication.id} --out <dir> - that publishes once and remembers the folder for this tool.`);
17733
17793
  }
17794
+ if (folder && !dryRun) await checkPublishFolder(graph, publication.id, folder, host);
17734
17795
  const selected = selectDocuments(source.documents, publication, summary.publications);
17735
17796
  const includeTexts = Object.values(publication.includes).map((concept) => source.documents.find((d) => d.concept.toLowerCase() === concept.toLowerCase())?.text ?? "");
17736
17797
  const bodies = [...selected.included.map((d) => d.text), ...includeTexts];
@@ -17761,7 +17822,7 @@ async function publish(graph, args, host) {
17761
17822
  }),
17762
17823
  unsettled
17763
17824
  });
17764
- const written = run.report.ok ? await writeSite(nodeSiteFolder(folder), run.bundle, {
17825
+ const written = run.report.ok && folder && !dryRun ? await writeSite(nodeSiteFolder(folder), run.bundle, {
17765
17826
  seeded: run.seeded,
17766
17827
  agentsMd: run.agentsMd
17767
17828
  }) : null;
@@ -17769,6 +17830,7 @@ async function publish(graph, args, host) {
17769
17830
  for (const e of run.report.excluded) excludedByReason[e.reason] = (excludedByReason[e.reason] ?? 0) + 1;
17770
17831
  return {
17771
17832
  ok: run.report.ok,
17833
+ ...dryRun ? { dryRun: true } : {},
17772
17834
  publication: run.report.publication,
17773
17835
  folder,
17774
17836
  written: written ? {
@@ -18500,7 +18562,7 @@ function createMcpServer(source, info) {
18500
18562
  "graph_insights says what the links show across the whole graph (the most-linked concepts, pages many documents ask for that do not exist yet, pages linked to nothing, groups of related concepts and the concepts bridging them), and graph_path finds how two concepts are connected. Use them to answer questions about the shape of the graph, or to suggest pages to write or link.",
18501
18563
  "Start with graph_info to see what you are connected to. read_documents reads several documents in one call. tasks lists tasks and set_task changes one (status, priority, due and scheduled dates) by document and line.",
18502
18564
  "A task reference is how a person hands you one task: its words, then an EtherPK address ending in #task=<line>-<fingerprint>. Pass the whole reference, or the address, to read_task for the task and its detail, to set_task (as reference) to change it, and to add_task_note to write what you did under it. It finds the task after lines are added above it or its tags change; if its words were edited it answers task_not_found, and you should ask the user for a fresh one.",
18503
- "Publishing: list_publications shows the publications this graph defines (a publication is a page whose frontmatter defines it - its outline is the site navigation) and the public documents none takes. A document is on a site when its frontmatter has public: true and names the publication in publications. create_publication and update_publication change the settings, and publish writes the site into the publish folder the user set for it on this machine with the etherpk-mcp publish command (the tool cannot choose a folder) and returns the report. Diagrams need a browser the user installs once with \"diagrams setup\".",
18565
+ "Publishing: list_publications shows the publications this graph defines (a publication is a page whose frontmatter defines it - its outline is the site navigation) and the public documents none takes. A document is on a site when its frontmatter has public: true and names the publication in publications. create_publication and update_publication change the settings, and publish writes the site into the publish folder the user set for it on this machine with the etherpk-mcp publish command (the tool cannot choose a folder) and returns the report. publish with dryRun: true renders and reports without writing and needs no folder: run it after editing a publication's pages and fix what it names. Diagrams need a browser the user installs once with \"diagrams setup\".",
18504
18566
  "Themes: a publication's look is a theme - Mustache templates, a stylesheet, a script and a manifest. list_themes shows the bundled ones (read-only) and the graph's own. read_theme writes a theme's files to a folder on this machine to read and edit. customise_publication_theme copies a publication's bundled theme into the graph and points the publication at the copy (create_theme copies any theme). write_theme_file, delete_theme_file and import_theme_folder change a graph theme. preview_theme renders a theme to a folder (with screenshots when a browser is set up) to check before publish. For a snippet such as an analytics script, an include slot (update_publication includes, e.g. head) filled by a page may be lighter than a theme copy.",
18505
18567
  "Images and files are assets: upload_asset adds a file from this machine and returns the markdown to paste into a document. read_asset writes an asset to a local file you can open, and list_assets shows the assets documents reference. An asset is available only where a document you can read references it (or you uploaded it this session). read_asset, read_theme and preview_theme write under the graph's downloads directory and return the path - name a folder relative to it, never elsewhere.",
18506
18568
  "The user documentation is at https://docs.etherpk.com."
@@ -18757,8 +18819,11 @@ function createMcpServer(source, info) {
18757
18819
  }, async (args) => run((graph) => updatePublication(graph, args, host)));
18758
18820
  server.registerTool("publish", {
18759
18821
  title: "Publish",
18760
- description: "Render a publication to its publish folder on this machine and return the report: what was included and why documents were left out, missing links, assets, warnings. The folder is the one the user set with \"etherpk-mcp publish --publication <id> --out <dir>\" (error \"no_publish_folder\" until then - the tool never chooses a folder). Pages with Mermaid diagrams or maps need the browser from \"diagrams setup\" (error \"chromium_unavailable\"). The report is this result, trimmed to counts and first entries - nothing of it is written into the folder, so the site never names the documents it leaves out. Publishing writes files - it does not deploy them.",
18761
- inputSchema: { id: z.string().min(1) }
18822
+ description: "Render a publication to its publish folder on this machine and return the report: what was included and why documents were left out, missing links, assets, warnings. The folder is the one the user set with \"etherpk-mcp publish --publication <id> --out <dir>\" (error \"no_publish_folder\" until then - the tool never chooses a folder), and a folder that is the graph's own folder, inside it or holding it is refused (error \"publish_folder_overlaps_graph\"). With dryRun: true it renders and reports but writes nothing and needs no folder: use it to check a publication after editing its pages, and fix what the report names. Pages with Mermaid diagrams or maps need the browser from \"diagrams setup\" (error \"chromium_unavailable\"), for a dry run too. The report is this result, trimmed to counts and first entries - nothing of it is written into the folder, so the site never names the documents it leaves out. Publishing writes files - it does not deploy them.",
18823
+ inputSchema: {
18824
+ id: z.string().min(1),
18825
+ dryRun: z.boolean().optional().describe("Render and report without writing anything. No publish folder needed.")
18826
+ }
18762
18827
  }, async (args) => run((graph) => publish(graph, args, host)));
18763
18828
  server.registerTool("list_themes", {
18764
18829
  title: "List themes",
@@ -19194,6 +19259,7 @@ async function runGraphHost(deps) {
19194
19259
  if (stopping) return answer({ etherpk: "stopping" });
19195
19260
  return startSession(socket, request, reply);
19196
19261
  case "publish":
19262
+ case "publish-check":
19197
19263
  if (stopping) return answer({ etherpk: "stopping" });
19198
19264
  return runPublish(request, answer);
19199
19265
  default: return answer({
@@ -25208,7 +25274,7 @@ function bindServeLifetime(deps) {
25208
25274
  * etherpk-mcp stop [--graph <id or name> [--sync-server <url>] | --folder <path> | --all]
25209
25275
  * etherpk-mcp logout [--sync-server <url> | --all]
25210
25276
  * etherpk-mcp semantic setup | status | remove
25211
- * etherpk-mcp publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir>]
25277
+ * etherpk-mcp publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir> | --dry-run]
25212
25278
  * etherpk-mcp diagrams setup | status
25213
25279
  *
25214
25280
  * One config file holds a login per Sync Server (ADR 0075). `--sync-server` names the one a
@@ -25259,8 +25325,9 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
25259
25325
  Serve a local graph folder the same way: no sign-in, no server. The agent gets search,
25260
25326
  backlinks, tasks and format-safe edits over the folder's markdown, alongside the files
25261
25327
  themselves. Edits made in an editor or by the agent directly are picked up as they land.
25262
- The folder must already be a graph (open it in EtherPK once) - its index is kept under
25263
- the cache directory, never in the folder.
25328
+ The folder must already be a graph, with pages/ and journals/ folders in it (EtherPK
25329
+ makes them when it opens a folder) - its index is kept under the cache directory, never
25330
+ in the folder.
25264
25331
  ${CMD} running
25265
25332
  List the graphs served on this computer: each one's background process, its agents and
25266
25333
  its log. A background process stops on its own once no agent has used it for five
@@ -25279,14 +25346,18 @@ var USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synce
25279
25346
  to a service to be embedded.
25280
25347
  ${CMD} semantic status | remove
25281
25348
  Whether it is set up here, or delete it (logout --all deletes it too).
25282
- ${CMD} publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir>]
25349
+ ${CMD} publish (--graph <id or name> | --folder <path>) --publication <id> [--out <dir> | --dry-run]
25283
25350
  Publish one publication of a graph to a folder on this machine and print the report.
25284
25351
  --out sets the publish folder for that graph and publication and is remembered: from
25285
25352
  then on this command without --out, and the agent's publish tool, write to the same
25286
- folder (the agent cannot choose one). The folder's owned files (.html at the top,
25287
- assets/, theme/, the search, feed and report files) are rewritten and their strays
25288
- removed - everything else in it is left alone. Publishing writes files - deploying them
25289
- (a git push, say) is yours. Run it from cron for a site that republishes itself.
25353
+ folder (the agent cannot choose one). It must be a folder of its own, outside the
25354
+ graph's folder: the folder's owned files (.html at the top, assets/, theme/, the search
25355
+ and feed files) are rewritten and their strays removed - everything else in it is left
25356
+ alone. Publishing writes files - deploying them (a git push, say) is yours. Run it from
25357
+ cron for a site that republishes itself.
25358
+ --dry-run renders and checks the publication and prints the whole report, with the
25359
+ names of the pages it mentions, but writes nothing and needs no folder. Keep its output
25360
+ out of public logs.
25290
25361
  ${CMD} diagrams setup
25291
25362
  Let a publish draw Mermaid diagrams and maps. Downloads a Chromium (about 170 MB) into the
25292
25363
  cache directory with Playwright's installer, once per computer - a publish whose pages hold
@@ -25694,14 +25765,22 @@ async function publishCommand(args) {
25694
25765
  const request = graphRequest("publish", args);
25695
25766
  const publication = args.publication?.trim();
25696
25767
  if (!publication) fail("publish needs --publication <id>. The agent's list_publications tool, or Settings → Publish in EtherPK, shows the ids.");
25697
- const found = await findGraph(request).catch((error) => fail(`etherpk-mcp: ${describeError(error)}`));
25768
+ const dryRun = args["dry-run"] === true;
25698
25769
  const out = args.out?.trim();
25699
- const { reply, socket } = await requestHost(launcherFor(found), {
25770
+ if (dryRun && out) fail("publish --dry-run writes nothing, so it takes no --out. Leave --out off, or drop --dry-run to publish.");
25771
+ const found = await findGraph(request).catch((error) => fail(`etherpk-mcp: ${describeError(error)}`));
25772
+ const env = publishSettings(process.env);
25773
+ const { reply, socket } = await requestHost(launcherFor(found), dryRun ? {
25774
+ etherpk: "publish-check",
25775
+ protocol: 1,
25776
+ publication,
25777
+ env
25778
+ } : {
25700
25779
  etherpk: "publish",
25701
25780
  protocol: 1,
25702
25781
  publication,
25703
25782
  ...out ? { out: resolve(out) } : {},
25704
- env: publishSettings(process.env)
25783
+ env
25705
25784
  }).catch((error) => fail(`etherpk-mcp: ${describeError(error)}`));
25706
25785
  socket.destroy();
25707
25786
  switch (reply.etherpk) {
@@ -25711,15 +25790,18 @@ async function publishCommand(args) {
25711
25790
  process.exitCode = reply.exitCode;
25712
25791
  return;
25713
25792
  case "refused": return fail(reply.message);
25714
- case "failed": return fail(`etherpk-mcp: ${reply.message}`);
25793
+ case "failed":
25794
+ if (dryRun && reply.message === "The request was not understood.") return fail(`etherpk-mcp: this graph is served on this computer by an older etherpk-mcp, which cannot run a dry run. Run: ${CMD} stop, then try again.`);
25795
+ return fail(`etherpk-mcp: ${reply.message}`);
25715
25796
  case "unsupported": return fail(`etherpk-mcp: this graph is served on this computer by etherpk-mcp ${reply.version}, which cannot run a publish for this version (${VERSION}). Run: ${CMD} stop, then publish again.`);
25716
25797
  default: return fail(`etherpk-mcp: the graph's background process gave an answer this version does not know ("${reply.etherpk}").`);
25717
25798
  }
25718
25799
  }
25719
25800
  /**
25720
25801
  * A command-line publish, as the graph's host runs it: the publication first, so a mistyped id is
25721
- * never remembered as a publish folder, then `--out` remembered, then the publish the agent's
25722
- * tool runs. The answer is what the command prints, in its own words.
25802
+ * never remembered as a publish folder, then `--out` checked against the graph's folder and
25803
+ * remembered, then the publish the agent's tool runs. The answer is what the command prints, in
25804
+ * its own words.
25723
25805
  */
25724
25806
  async function publishInHost(graph, request) {
25725
25807
  const env = withPublishSettings(process.env, request.env ?? {});
@@ -25730,10 +25812,23 @@ async function publishInHost(graph, request) {
25730
25812
  };
25731
25813
  const notes = [];
25732
25814
  try {
25815
+ if (request.etherpk === "publish-check") {
25816
+ const result = await publish(graph, {
25817
+ id: request.publication,
25818
+ dryRun: true
25819
+ }, host);
25820
+ return {
25821
+ etherpk: "published",
25822
+ output: result,
25823
+ notes: [],
25824
+ exitCode: result.ok ? 0 : 1
25825
+ };
25826
+ }
25733
25827
  await findPublication(graph, request.publication, host);
25734
25828
  const configPath = defaultPublishFoldersPath(env);
25735
25829
  const key = publishGraphKey(graph.backend, graph.graphId);
25736
25830
  if (request.out) {
25831
+ await checkPublishFolder(graph, request.publication, request.out, host);
25737
25832
  await writePublishFolders(configPath, withPublishFolder(await readPublishFolders(configPath), key, request.publication, request.out));
25738
25833
  notes.push(`etherpk-mcp: publish folder for "${request.publication}" of "${graph.name}" set to ${request.out} (remembered in ${configPath}).`);
25739
25834
  } else if (!publishFolderOf(await readPublishFolders(configPath), key, request.publication)) return {
@@ -26256,6 +26351,7 @@ async function main() {
26256
26351
  "no-semantic": { type: "boolean" },
26257
26352
  publication: { type: "string" },
26258
26353
  out: { type: "string" },
26354
+ "dry-run": { type: "boolean" },
26259
26355
  all: { type: "boolean" },
26260
26356
  name: { type: "string" },
26261
26357
  help: {