@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/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { McpServer } from "@modelcontextprotocol/server";
3
3
 
4
4
  // src/lib/version.ts
5
- var SERVER_VERSION = true ? "1.2.0" : "dev-mode";
5
+ var SERVER_VERSION = true ? "1.4.0" : "dev-mode";
6
6
  var SERVER_NAME = "json-to-office";
7
7
  var PACKAGE_NAME = "@json-to-office/mcp-server";
8
8
 
@@ -165,6 +165,9 @@ function outputSchema(properties, required = []) {
165
165
  import {
166
166
  runWithDiagnosticSink
167
167
  } from "@json-to-office/jto-ops";
168
+ import {
169
+ RENDERER_DEPENDENCY_MISSING
170
+ } from "@json-to-office/shared";
168
171
  import { ValueErrorType } from "@sinclair/typebox/errors";
169
172
  var ERROR_CODES = {
170
173
  /** An exception escaped a tool handler. Always a bug here, never the caller's. */
@@ -308,7 +311,11 @@ function toolResult(payload) {
308
311
  structuredContent: payload
309
312
  };
310
313
  }
311
- var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set(["RendererDependencyMissingError"]);
314
+ var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set([RENDERER_DEPENDENCY_MISSING]);
315
+ function stackAllowed() {
316
+ const flag = process.env.JTO_MCP_DEBUG_STACKS;
317
+ return flag === "1" || flag === "true";
318
+ }
312
319
  function hostNote(text, tone = "muted") {
313
320
  return {
314
321
  // Never `error`. The body has already decided `ok` by the time a note
@@ -359,7 +366,7 @@ async function guarded(body) {
359
366
  return withHostNotes(
360
367
  failure(code, message2, {
361
368
  context: {
362
- ...error instanceof Error && error.stack !== void 0 && { stack: error.stack }
369
+ ...stackAllowed() && error instanceof Error && error.stack !== void 0 && { stack: error.stack }
363
370
  }
364
371
  }),
365
372
  notes
@@ -602,7 +609,7 @@ function register(server, deps) {
602
609
  "jto_info",
603
610
  {
604
611
  title: "Server info",
605
- 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.",
612
+ 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.",
606
613
  annotations: { readOnlyHint: true, openWorldHint: false },
607
614
  inputSchema: S({
608
615
  type: "object",
@@ -654,10 +661,32 @@ function register(server, deps) {
654
661
  rendererIds: {
655
662
  type: "array",
656
663
  items: { type: "string" },
657
- description: "Defaults first."
664
+ description: "Defaults first. Registered, which is not the same as usable \u2014 read `renderers` before picking one."
665
+ },
666
+ renderers: {
667
+ type: "array",
668
+ 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.",
669
+ items: {
670
+ type: "object",
671
+ properties: {
672
+ id: { type: "string" },
673
+ default: { type: "boolean" },
674
+ available: { type: "boolean" },
675
+ reason: { type: "string" },
676
+ installHint: { type: "string" }
677
+ },
678
+ required: ["id", "default", "available"],
679
+ additionalProperties: false
680
+ }
658
681
  }
659
682
  },
660
- required: ["name", "extension", "label", "rendererIds"],
683
+ required: [
684
+ "name",
685
+ "extension",
686
+ "label",
687
+ "rendererIds",
688
+ "renderers"
689
+ ],
661
690
  additionalProperties: false
662
691
  }
663
692
  },
@@ -713,9 +742,21 @@ function register(server, deps) {
713
742
  const formats = await Promise.all(
714
743
  FORMAT_NAMES.map(async (name) => {
715
744
  const adapter = deps.getAdapter(name);
716
- let rendererIds = [];
745
+ let renderers = [];
717
746
  try {
718
- rendererIds = [...await adapter.rendererIds()];
747
+ renderers = (await adapter.rendererStatuses()).map(
748
+ (status) => ({
749
+ id: status.id,
750
+ default: status.default,
751
+ available: status.available,
752
+ ...status.reason !== void 0 && {
753
+ reason: status.reason
754
+ },
755
+ ...status.installHint !== void 0 && {
756
+ installHint: status.installHint
757
+ }
758
+ })
759
+ );
719
760
  } catch (error) {
720
761
  diagnostics.push(
721
762
  diagnostic(
@@ -725,11 +766,32 @@ function register(server, deps) {
725
766
  )
726
767
  );
727
768
  }
769
+ for (const renderer of renderers) {
770
+ if (renderer.available) continue;
771
+ diagnostics.push(
772
+ diagnostic(
773
+ ERROR_CODES.DEPENDENCY_MISSING,
774
+ `The "${renderer.id}" ${name} renderer is registered but cannot load on this host, so every render through it will fail.`,
775
+ {
776
+ severity: "warning",
777
+ ...renderer.installHint && {
778
+ suggestion: `Install its backend: ${renderer.installHint}. Until then use one of: ${renderers.filter((entry) => entry.available).map((entry) => `"${entry.id}"`).join(", ")}.`
779
+ },
780
+ context: {
781
+ format: name,
782
+ renderer: renderer.id,
783
+ ...renderer.reason && { reason: renderer.reason }
784
+ }
785
+ }
786
+ )
787
+ );
788
+ }
728
789
  return {
729
790
  name: adapter.name,
730
791
  extension: adapter.extension,
731
792
  label: adapter.label,
732
- rendererIds
793
+ rendererIds: renderers.map((renderer) => renderer.id),
794
+ renderers
733
795
  };
734
796
  })
735
797
  );
@@ -1117,8 +1179,16 @@ async function catalogFormat(format, deps, diagnostics) {
1117
1179
  const schemas = formatSchemas(format);
1118
1180
  const adapter = deps.getAdapter(format);
1119
1181
  let rendererIds = [];
1182
+ const availability = /* @__PURE__ */ new Map();
1183
+ const installHints = /* @__PURE__ */ new Map();
1184
+ let probed = false;
1120
1185
  try {
1121
- rendererIds = [...await adapter.rendererIds()];
1186
+ for (const status of await adapter.rendererStatuses()) {
1187
+ rendererIds.push(status.id);
1188
+ availability.set(status.id, status.available);
1189
+ if (status.installHint) installHints.set(status.id, status.installHint);
1190
+ }
1191
+ probed = true;
1122
1192
  } catch (error) {
1123
1193
  diagnostics.push(
1124
1194
  diagnostic(
@@ -1234,6 +1304,14 @@ async function catalogFormat(format, deps, diagnostics) {
1234
1304
  renderers: orderedIds.map((id, index) => ({
1235
1305
  id,
1236
1306
  default: index === 0,
1307
+ // Three cases, and they are not the same. A probed renderer answers for
1308
+ // itself. A profile the cores never registered has no status but is
1309
+ // already reported as drift above, so calling it unavailable would be a
1310
+ // second, worse description of that. And a probe that threw knows
1311
+ // nothing about any of them — reporting those as usable would contradict
1312
+ // the diagnostic pushed beside them.
1313
+ available: availability.get(id) ?? probed,
1314
+ ...installHints.has(id) && { installHint: installHints.get(id) },
1237
1315
  components: [...byRenderer.get(id)?.components.keys() ?? []].sort(),
1238
1316
  unsupported: allNames.filter((name) => !byRenderer.get(id)?.components.has(name)).sort()
1239
1317
  })),
@@ -1303,6 +1381,14 @@ function register2(server, deps) {
1303
1381
  properties: {
1304
1382
  id: { type: "string" },
1305
1383
  default: { type: "boolean" },
1384
+ available: {
1385
+ type: "boolean",
1386
+ 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."
1387
+ },
1388
+ installHint: {
1389
+ type: "string",
1390
+ description: "The command that would make an unavailable renderer available."
1391
+ },
1306
1392
  components: {
1307
1393
  type: "array",
1308
1394
  items: { type: "string" }
@@ -1313,7 +1399,13 @@ function register2(server, deps) {
1313
1399
  description: "Components another renderer of this format accepts and this one does not."
1314
1400
  }
1315
1401
  },
1316
- required: ["id", "default", "components", "unsupported"],
1402
+ required: [
1403
+ "id",
1404
+ "default",
1405
+ "available",
1406
+ "components",
1407
+ "unsupported"
1408
+ ],
1317
1409
  additionalProperties: false
1318
1410
  }
1319
1411
  },
@@ -1756,6 +1848,39 @@ function withRenderer(document, renderer) {
1756
1848
  }
1757
1849
  return { ...document, renderer };
1758
1850
  }
1851
+ function effectiveRenderer(document, override2) {
1852
+ if (override2 !== void 0) return override2;
1853
+ if (typeof document === "object" && document !== null) {
1854
+ const declared = document.renderer;
1855
+ if (typeof declared === "string") return declared;
1856
+ }
1857
+ return void 0;
1858
+ }
1859
+ async function rendererAvailability(adapter, document, override2) {
1860
+ const wanted = effectiveRenderer(document, override2);
1861
+ let statuses;
1862
+ try {
1863
+ statuses = await adapter.rendererStatuses();
1864
+ } catch {
1865
+ return void 0;
1866
+ }
1867
+ const status = wanted ? statuses.find((entry) => entry.id === wanted) : statuses.find((entry) => entry.default);
1868
+ if (!status || status.available) return void 0;
1869
+ const usable = statuses.filter((entry) => entry.available).map((entry) => `"${entry.id}"`);
1870
+ return diagnostic(
1871
+ ERROR_CODES.DEPENDENCY_MISSING,
1872
+ `This document validates against the "${status.id}" ${adapter.name} renderer, but that renderer cannot load on this host \u2014 generating with it will fail.`,
1873
+ {
1874
+ severity: "warning",
1875
+ suggestion: status.installHint ? `Install its backend: ${status.installHint}.${usable.length > 0 ? ` Or render with ${usable.join(" or ")}.` : ""}` : `Render with ${usable.join(" or ")} instead.`,
1876
+ context: {
1877
+ format: adapter.name,
1878
+ renderer: status.id,
1879
+ ...status.reason && { reason: status.reason }
1880
+ }
1881
+ }
1882
+ );
1883
+ }
1759
1884
 
1760
1885
  // src/lib/workspace-store.ts
1761
1886
  var unavailable = () => failure(
@@ -1879,7 +2004,7 @@ function register4(server, deps) {
1879
2004
  "jto_validate",
1880
2005
  {
1881
2006
  title: "Validate a document",
1882
- 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.",
2007
+ 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.",
1883
2008
  annotations: { readOnlyHint: true, openWorldHint: false },
1884
2009
  inputSchema: S({
1885
2010
  type: "object",
@@ -1940,7 +2065,15 @@ function register4(server, deps) {
1940
2065
  const result = adapter.validateDocument(
1941
2066
  withRenderer(resolved.document, args.renderer)
1942
2067
  );
1943
- const all = validationDiagnostics(result.errors);
2068
+ const unavailable2 = await rendererAvailability(
2069
+ adapter,
2070
+ resolved.document,
2071
+ args.renderer
2072
+ );
2073
+ const all = [
2074
+ ...validationDiagnostics(result.errors),
2075
+ ...unavailable2 ? [unavailable2] : []
2076
+ ];
1944
2077
  const counts = countDiagnostics(all);
1945
2078
  const { kept, truncated } = capDiagnostics(
1946
2079
  all,
@@ -2830,6 +2963,9 @@ import crypto2 from "crypto";
2830
2963
  import { promises as fs6 } from "fs";
2831
2964
  import os2 from "os";
2832
2965
  import path5 from "path";
2966
+ import {
2967
+ RENDERER_DEPENDENCY_MISSING as RENDERER_DEPENDENCY_MISSING2
2968
+ } from "@json-to-office/shared";
2833
2969
  import { getFontStager } from "@json-to-office/jto-ops";
2834
2970
 
2835
2971
  // src/preview/cache-key.ts
@@ -3161,6 +3297,15 @@ function renderFailure(stage, detail, context = {}) {
3161
3297
  { suggestion, context: { stage, ...context } }
3162
3298
  );
3163
3299
  }
3300
+ function buildFailure(error) {
3301
+ if (error instanceof Error && error.name === RENDERER_DEPENDENCY_MISSING2) {
3302
+ return failure(ERROR_CODES.DEPENDENCY_MISSING, message(error), {
3303
+ suggestion: "Install the renderer's backend, or re-run with a renderer jto_info reports as available. The document is not at fault.",
3304
+ context: { stage: "build" }
3305
+ });
3306
+ }
3307
+ return renderFailure("build", message(error));
3308
+ }
3164
3309
  var PNG_SIGNATURE = Buffer.from([
3165
3310
  137,
3166
3311
  80,
@@ -3367,9 +3512,10 @@ async function renderPreview(options) {
3367
3512
  converters = versions;
3368
3513
  } catch (error) {
3369
3514
  if (signal?.aborted) return cancelled2();
3515
+ const build = buildFailure(error);
3370
3516
  return failureFrom([
3371
- ...validationDiagnostics2(options.getAdapter(format), document),
3372
- ...renderFailure("build", message(error)).diagnostics
3517
+ ...build.diagnostics[0]?.code === ERROR_CODES.DEPENDENCY_MISSING ? [] : validationDiagnostics2(options.getAdapter(format), document),
3518
+ ...build.diagnostics
3373
3519
  ]);
3374
3520
  }
3375
3521
  const generateMs = elapsed(generateStarted);