@json-to-office/mcp-server 1.2.0 → 1.4.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/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
7
7
  import { McpServer } from "@modelcontextprotocol/server";
8
8
 
9
9
  // src/lib/version.ts
10
- var SERVER_VERSION = true ? "1.2.0" : "dev-mode";
10
+ var SERVER_VERSION = true ? "1.4.0" : "dev-mode";
11
11
  var SERVER_NAME = "json-to-office";
12
12
  var PACKAGE_NAME = "@json-to-office/mcp-server";
13
13
 
@@ -170,6 +170,9 @@ function outputSchema(properties, required = []) {
170
170
  import {
171
171
  runWithDiagnosticSink
172
172
  } from "@json-to-office/jto-ops";
173
+ import {
174
+ RENDERER_DEPENDENCY_MISSING
175
+ } from "@json-to-office/shared";
173
176
  import { ValueErrorType } from "@sinclair/typebox/errors";
174
177
  var ERROR_CODES = {
175
178
  /** An exception escaped a tool handler. Always a bug here, never the caller's. */
@@ -313,7 +316,11 @@ function toolResult(payload) {
313
316
  structuredContent: payload
314
317
  };
315
318
  }
316
- var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set(["RendererDependencyMissingError"]);
319
+ var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set([RENDERER_DEPENDENCY_MISSING]);
320
+ function stackAllowed() {
321
+ const flag = process.env.JTO_MCP_DEBUG_STACKS;
322
+ return flag === "1" || flag === "true";
323
+ }
317
324
  function hostNote(text, tone = "muted") {
318
325
  return {
319
326
  // Never `error`. The body has already decided `ok` by the time a note
@@ -364,7 +371,7 @@ async function guarded(body) {
364
371
  return withHostNotes(
365
372
  failure(code, message2, {
366
373
  context: {
367
- ...error instanceof Error && error.stack !== void 0 && { stack: error.stack }
374
+ ...stackAllowed() && error instanceof Error && error.stack !== void 0 && { stack: error.stack }
368
375
  }
369
376
  }),
370
377
  notes
@@ -607,7 +614,7 @@ function register(server, deps) {
607
614
  "jto_info",
608
615
  {
609
616
  title: "Server info",
610
- description: "Versions, supported formats and renderer ids, workspace availability, output-root and size limits, and whether the optional host dependencies (LibreOffice and poppler for jto_preview, a Highcharts export server for the DOCX `highcharts` component) are present on this host. Call this first.",
617
+ description: "Versions, supported formats with each renderer and whether its backend loads here, workspace availability, output-root and size limits, and whether the optional host dependencies (LibreOffice and poppler for jto_preview, a Highcharts export server for the DOCX `highcharts` component) are present on this host. Call this first.",
611
618
  annotations: { readOnlyHint: true, openWorldHint: false },
612
619
  inputSchema: S({
613
620
  type: "object",
@@ -659,10 +666,32 @@ function register(server, deps) {
659
666
  rendererIds: {
660
667
  type: "array",
661
668
  items: { type: "string" },
662
- description: "Defaults first."
669
+ description: "Defaults first. Registered, which is not the same as usable \u2014 read `renderers` before picking one."
670
+ },
671
+ renderers: {
672
+ type: "array",
673
+ description: "Every registered renderer with whether its backend loads on this host. A renderer with `available: false` will fail every render until `installHint` is run.",
674
+ items: {
675
+ type: "object",
676
+ properties: {
677
+ id: { type: "string" },
678
+ default: { type: "boolean" },
679
+ available: { type: "boolean" },
680
+ reason: { type: "string" },
681
+ installHint: { type: "string" }
682
+ },
683
+ required: ["id", "default", "available"],
684
+ additionalProperties: false
685
+ }
663
686
  }
664
687
  },
665
- required: ["name", "extension", "label", "rendererIds"],
688
+ required: [
689
+ "name",
690
+ "extension",
691
+ "label",
692
+ "rendererIds",
693
+ "renderers"
694
+ ],
666
695
  additionalProperties: false
667
696
  }
668
697
  },
@@ -718,9 +747,21 @@ function register(server, deps) {
718
747
  const formats = await Promise.all(
719
748
  FORMAT_NAMES.map(async (name) => {
720
749
  const adapter = deps.getAdapter(name);
721
- let rendererIds = [];
750
+ let renderers = [];
722
751
  try {
723
- rendererIds = [...await adapter.rendererIds()];
752
+ renderers = (await adapter.rendererStatuses()).map(
753
+ (status) => ({
754
+ id: status.id,
755
+ default: status.default,
756
+ available: status.available,
757
+ ...status.reason !== void 0 && {
758
+ reason: status.reason
759
+ },
760
+ ...status.installHint !== void 0 && {
761
+ installHint: status.installHint
762
+ }
763
+ })
764
+ );
724
765
  } catch (error) {
725
766
  diagnostics.push(
726
767
  diagnostic(
@@ -730,11 +771,32 @@ function register(server, deps) {
730
771
  )
731
772
  );
732
773
  }
774
+ for (const renderer of renderers) {
775
+ if (renderer.available) continue;
776
+ diagnostics.push(
777
+ diagnostic(
778
+ ERROR_CODES.DEPENDENCY_MISSING,
779
+ `The "${renderer.id}" ${name} renderer is registered but cannot load on this host, so every render through it will fail.`,
780
+ {
781
+ severity: "warning",
782
+ ...renderer.installHint && {
783
+ suggestion: `Install its backend: ${renderer.installHint}. Until then use one of: ${renderers.filter((entry) => entry.available).map((entry) => `"${entry.id}"`).join(", ")}.`
784
+ },
785
+ context: {
786
+ format: name,
787
+ renderer: renderer.id,
788
+ ...renderer.reason && { reason: renderer.reason }
789
+ }
790
+ }
791
+ )
792
+ );
793
+ }
733
794
  return {
734
795
  name: adapter.name,
735
796
  extension: adapter.extension,
736
797
  label: adapter.label,
737
- rendererIds
798
+ rendererIds: renderers.map((renderer) => renderer.id),
799
+ renderers
738
800
  };
739
801
  })
740
802
  );
@@ -1122,8 +1184,16 @@ async function catalogFormat(format, deps, diagnostics) {
1122
1184
  const schemas = formatSchemas(format);
1123
1185
  const adapter = deps.getAdapter(format);
1124
1186
  let rendererIds = [];
1187
+ const availability = /* @__PURE__ */ new Map();
1188
+ const installHints = /* @__PURE__ */ new Map();
1189
+ let probed = false;
1125
1190
  try {
1126
- rendererIds = [...await adapter.rendererIds()];
1191
+ for (const status of await adapter.rendererStatuses()) {
1192
+ rendererIds.push(status.id);
1193
+ availability.set(status.id, status.available);
1194
+ if (status.installHint) installHints.set(status.id, status.installHint);
1195
+ }
1196
+ probed = true;
1127
1197
  } catch (error) {
1128
1198
  diagnostics.push(
1129
1199
  diagnostic(
@@ -1239,6 +1309,14 @@ async function catalogFormat(format, deps, diagnostics) {
1239
1309
  renderers: orderedIds.map((id, index) => ({
1240
1310
  id,
1241
1311
  default: index === 0,
1312
+ // Three cases, and they are not the same. A probed renderer answers for
1313
+ // itself. A profile the cores never registered has no status but is
1314
+ // already reported as drift above, so calling it unavailable would be a
1315
+ // second, worse description of that. And a probe that threw knows
1316
+ // nothing about any of them — reporting those as usable would contradict
1317
+ // the diagnostic pushed beside them.
1318
+ available: availability.get(id) ?? probed,
1319
+ ...installHints.has(id) && { installHint: installHints.get(id) },
1242
1320
  components: [...byRenderer.get(id)?.components.keys() ?? []].sort(),
1243
1321
  unsupported: allNames.filter((name) => !byRenderer.get(id)?.components.has(name)).sort()
1244
1322
  })),
@@ -1308,6 +1386,14 @@ function register2(server, deps) {
1308
1386
  properties: {
1309
1387
  id: { type: "string" },
1310
1388
  default: { type: "boolean" },
1389
+ available: {
1390
+ type: "boolean",
1391
+ description: "Whether this renderer's backend loads on this host. A renderer that is registered but unavailable accepts the components below and then fails every render."
1392
+ },
1393
+ installHint: {
1394
+ type: "string",
1395
+ description: "The command that would make an unavailable renderer available."
1396
+ },
1311
1397
  components: {
1312
1398
  type: "array",
1313
1399
  items: { type: "string" }
@@ -1318,7 +1404,13 @@ function register2(server, deps) {
1318
1404
  description: "Components another renderer of this format accepts and this one does not."
1319
1405
  }
1320
1406
  },
1321
- required: ["id", "default", "components", "unsupported"],
1407
+ required: [
1408
+ "id",
1409
+ "default",
1410
+ "available",
1411
+ "components",
1412
+ "unsupported"
1413
+ ],
1322
1414
  additionalProperties: false
1323
1415
  }
1324
1416
  },
@@ -1758,6 +1850,39 @@ function withRenderer(document, renderer) {
1758
1850
  }
1759
1851
  return { ...document, renderer };
1760
1852
  }
1853
+ function effectiveRenderer(document, override2) {
1854
+ if (override2 !== void 0) return override2;
1855
+ if (typeof document === "object" && document !== null) {
1856
+ const declared = document.renderer;
1857
+ if (typeof declared === "string") return declared;
1858
+ }
1859
+ return void 0;
1860
+ }
1861
+ async function rendererAvailability(adapter, document, override2) {
1862
+ const wanted = effectiveRenderer(document, override2);
1863
+ let statuses;
1864
+ try {
1865
+ statuses = await adapter.rendererStatuses();
1866
+ } catch {
1867
+ return void 0;
1868
+ }
1869
+ const status = wanted ? statuses.find((entry) => entry.id === wanted) : statuses.find((entry) => entry.default);
1870
+ if (!status || status.available) return void 0;
1871
+ const usable = statuses.filter((entry) => entry.available).map((entry) => `"${entry.id}"`);
1872
+ return diagnostic(
1873
+ ERROR_CODES.DEPENDENCY_MISSING,
1874
+ `This document validates against the "${status.id}" ${adapter.name} renderer, but that renderer cannot load on this host \u2014 generating with it will fail.`,
1875
+ {
1876
+ severity: "warning",
1877
+ suggestion: status.installHint ? `Install its backend: ${status.installHint}.${usable.length > 0 ? ` Or render with ${usable.join(" or ")}.` : ""}` : `Render with ${usable.join(" or ")} instead.`,
1878
+ context: {
1879
+ format: adapter.name,
1880
+ renderer: status.id,
1881
+ ...status.reason && { reason: status.reason }
1882
+ }
1883
+ }
1884
+ );
1885
+ }
1761
1886
 
1762
1887
  // src/lib/workspace-store.ts
1763
1888
  var unavailable = () => failure(
@@ -1867,7 +1992,7 @@ function register4(server, deps) {
1867
1992
  "jto_validate",
1868
1993
  {
1869
1994
  title: "Validate a document",
1870
- description: "Check a document against its format schema and report every defect as a path-addressed diagnostic. Paths are RFC 6901 JSON Pointers into the document you passed, so they can be used directly as patch targets; codes are the stable `E_`/`W_` vocabulary, e.g. `E_REQUIRED_PROPERTY`, `E_UNEXPECTED_PROPERTY`, `E_TYPE_MISMATCH`, `E_UNKNOWN_COMPONENT`. `ok` mirrors the gate generation applies: schema and semantic errors block it \u2014 the semantic rules the published JSON Schema cannot state, such as a text component needing one of `text`/`runs`, are checked here and only here \u2014 while renderer-profile findings (code `W_UNSUPPORTED_RENDERER_FEATURE`) come back as warnings because the renderer, not the schema, has the last word on those. A broken document is a normal result with `ok: false`, never an error.",
1995
+ description: "Check a document against its format schema and report every defect as a path-addressed diagnostic. Paths are RFC 6901 JSON Pointers into the document you passed, so they can be used directly as patch targets; codes are the stable `E_`/`W_` vocabulary, e.g. `E_REQUIRED_PROPERTY`, `E_UNEXPECTED_PROPERTY`, `E_TYPE_MISMATCH`, `E_UNKNOWN_COMPONENT`. `ok` mirrors the gate generation applies: schema and semantic errors block it \u2014 the semantic rules the published JSON Schema cannot state, such as a text component needing one of `text`/`runs`, are checked here and only here \u2014 while renderer-profile findings (code `W_UNSUPPORTED_RENDERER_FEATURE`) come back as warnings because the renderer, not the schema, has the last word on those. A broken document is a normal result with `ok: false`, never an error. A renderer whose backend is not installed on this host is reported here too, as a `E_DEPENDENCY_MISSING` warning \u2014 the document may be fine and the host merely incomplete.",
1871
1996
  annotations: { readOnlyHint: true, openWorldHint: false },
1872
1997
  inputSchema: S({
1873
1998
  type: "object",
@@ -1928,7 +2053,15 @@ function register4(server, deps) {
1928
2053
  const result = adapter.validateDocument(
1929
2054
  withRenderer(resolved.document, args.renderer)
1930
2055
  );
1931
- const all = validationDiagnostics(result.errors);
2056
+ const unavailable2 = await rendererAvailability(
2057
+ adapter,
2058
+ resolved.document,
2059
+ args.renderer
2060
+ );
2061
+ const all = [
2062
+ ...validationDiagnostics(result.errors),
2063
+ ...unavailable2 ? [unavailable2] : []
2064
+ ];
1932
2065
  const counts = countDiagnostics(all);
1933
2066
  const { kept, truncated } = capDiagnostics(
1934
2067
  all,
@@ -2818,6 +2951,9 @@ import crypto2 from "crypto";
2818
2951
  import { promises as fs6 } from "fs";
2819
2952
  import os2 from "os";
2820
2953
  import path5 from "path";
2954
+ import {
2955
+ RENDERER_DEPENDENCY_MISSING as RENDERER_DEPENDENCY_MISSING2
2956
+ } from "@json-to-office/shared";
2821
2957
  import { getFontStager } from "@json-to-office/jto-ops";
2822
2958
 
2823
2959
  // src/preview/cache-key.ts
@@ -3149,6 +3285,15 @@ function renderFailure(stage, detail, context = {}) {
3149
3285
  { suggestion, context: { stage, ...context } }
3150
3286
  );
3151
3287
  }
3288
+ function buildFailure(error) {
3289
+ if (error instanceof Error && error.name === RENDERER_DEPENDENCY_MISSING2) {
3290
+ return failure(ERROR_CODES.DEPENDENCY_MISSING, message(error), {
3291
+ suggestion: "Install the renderer's backend, or re-run with a renderer jto_info reports as available. The document is not at fault.",
3292
+ context: { stage: "build" }
3293
+ });
3294
+ }
3295
+ return renderFailure("build", message(error));
3296
+ }
3152
3297
  var PNG_SIGNATURE = Buffer.from([
3153
3298
  137,
3154
3299
  80,
@@ -3355,9 +3500,10 @@ async function renderPreview(options) {
3355
3500
  converters = versions;
3356
3501
  } catch (error) {
3357
3502
  if (signal?.aborted) return cancelled2();
3503
+ const build = buildFailure(error);
3358
3504
  return failureFrom([
3359
- ...validationDiagnostics2(options.getAdapter(format), document),
3360
- ...renderFailure("build", message(error)).diagnostics
3505
+ ...build.diagnostics[0]?.code === ERROR_CODES.DEPENDENCY_MISSING ? [] : validationDiagnostics2(options.getAdapter(format), document),
3506
+ ...build.diagnostics
3361
3507
  ]);
3362
3508
  }
3363
3509
  const generateMs = elapsed(generateStarted);