@json-to-office/mcp-server 1.4.0 → 1.5.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 CHANGED
@@ -164,12 +164,14 @@ Nested components collapse to their names on purpose — describe those separate
164
164
 
165
165
  ### `jto_validate`
166
166
 
167
- **In** — `format` (required); document source; `renderer` (validate against this profile instead of the document's own, for this check only); `maxDiagnostics` (1–1000, default 100 — errors are kept ahead of warnings when the cap bites).
167
+ **In** — `format` (required); document source; `renderer` (validate against this profile instead of the document's own, for this check only); `quality` `{profile?, policy?}`; `maxDiagnostics` (1–1000, default 100 — errors are kept ahead of warnings when the cap bites).
168
168
 
169
169
  **Out** — `valid`, `format`, `renderer` (when one was requested), `source` `{origin, handle?, revision?}`, `counts` `{error, warning, info}` (before any cap), `truncated`.
170
170
 
171
171
  `ok` mirrors the gate generation applies: schema and semantic errors block it, renderer-profile findings (`W_UNSUPPORTED_RENDERER_FEATURE`) come back as warnings, because the renderer has the last word on those.
172
172
 
173
+ Design-quality findings ride the same envelope as `W_QUALITY_*` warnings and infos: an undeclared slide canvas, estimated text overflow, overcrowding, unreadable type, table overflow, or a skipped heading. They carry category, certainty, evidence, suggestion, and optional fixes. They are advisory by default; `quality.policy.gate: "warning"` makes warning-or-higher findings set `ok: false` without turning the tool call into a protocol error.
174
+
173
175
  The gate, not `jto://schema/{format}/document`. The two agree except on component nodes whose props are all optional, where the generated schema asks for a `props: {}` the validator and both renderers treat as omissible; validate follows the renderer, because that is what `jto_generate` runs.
174
176
 
175
177
  ### `jto_generate`
@@ -229,6 +231,7 @@ The same catalogues, for clients that read resources. URIs are stable.
229
231
  | `jto://catalog` | The resource form of `jto_discover`: every format, in full. |
230
232
  | `jto://renderers` | Renderer ids per format, which is default, what each profile can draw. |
231
233
  | `jto://themes` | Built-in theme names per format. |
234
+ | `jto://themes/values` | What each built-in theme actually is: palette, fonts, style tables. |
232
235
  | `jto://templates` | Every starter document. |
233
236
  | `jto://schema/docx/document` | Generated JSON Schema for a complete `.docx` document, by renderer. |
234
237
  | `jto://schema/pptx/document` | The same for `.pptx`. |
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.4.0" : "dev-mode";
10
+ var SERVER_VERSION = true ? "1.5.0" : "dev-mode";
11
11
  var SERVER_NAME = "json-to-office";
12
12
  var PACKAGE_NAME = "@json-to-office/mcp-server";
13
13
 
@@ -221,6 +221,8 @@ var ERROR_CODES = {
221
221
  HOST_NOTE: "W_HOST_NOTE",
222
222
  /** A generation warning the core raised without a code of its own. */
223
223
  GENERATION: "W_GENERATION",
224
+ /** A design-quality rule threw, so its whole class of findings is missing. */
225
+ QUALITY_RULE_ERROR: "W_QUALITY_RULE_ERROR",
224
226
  /** A required host binary (LibreOffice, poppler) is absent. */
225
227
  DEPENDENCY_MISSING: "E_DEPENDENCY_MISSING",
226
228
  /** The client cancelled the request. */
@@ -317,6 +319,25 @@ function toolResult(payload) {
317
319
  };
318
320
  }
319
321
  var HOST_DEPENDENCY_ERRORS = /* @__PURE__ */ new Set([RENDERER_DEPENDENCY_MISSING]);
322
+ function qualityOptionDiagnostic(error) {
323
+ const code = qualityCallerCode(error);
324
+ if (code === void 0 || code === ERROR_CODES.INVALID_DOCUMENT) {
325
+ return void 0;
326
+ }
327
+ return diagnostic(
328
+ code,
329
+ error instanceof Error ? error.message : String(error)
330
+ );
331
+ }
332
+ function qualityCallerCode(error) {
333
+ const code = error?.code;
334
+ if (code === "QUALITY_PROFILE_INCOMPATIBLE")
335
+ return OPTION_ERROR_CODES.INVALID_QUALITY_PROFILE;
336
+ if (code === "QUALITY_POLICY_INVALID")
337
+ return OPTION_ERROR_CODES.INVALID_QUALITY_POLICY;
338
+ if (code === "QUALITY_GATE_FAILED") return ERROR_CODES.INVALID_DOCUMENT;
339
+ return void 0;
340
+ }
320
341
  function stackAllowed() {
321
342
  const flag = process.env.JTO_MCP_DEBUG_STACKS;
322
343
  return flag === "1" || flag === "true";
@@ -367,7 +388,7 @@ async function guarded(body) {
367
388
  return withHostNotes(result, notes);
368
389
  } catch (error) {
369
390
  const message2 = error instanceof Error ? error.message : String(error);
370
- const code = error instanceof Error && HOST_DEPENDENCY_ERRORS.has(error.name) ? ERROR_CODES.DEPENDENCY_MISSING : ERROR_CODES.INTERNAL;
391
+ const code = qualityCallerCode(error) ?? (error instanceof Error && HOST_DEPENDENCY_ERRORS.has(error.name) ? ERROR_CODES.DEPENDENCY_MISSING : ERROR_CODES.INTERNAL);
371
392
  return withHostNotes(
372
393
  failure(code, message2, {
373
394
  context: {
@@ -386,7 +407,11 @@ var OPTION_ERROR_CODES = {
386
407
  /** `themePath` is not a data-only JSON theme path. */
387
408
  INVALID_THEME_PATH: "E_INVALID_THEME_PATH",
388
409
  /** The tool does not support the requested format. */
389
- UNSUPPORTED_FORMAT: "E_UNSUPPORTED_FORMAT"
410
+ UNSUPPORTED_FORMAT: "E_UNSUPPORTED_FORMAT",
411
+ /** `quality.profile` does not cover the format or renderer of this run. */
412
+ INVALID_QUALITY_PROFILE: "E_INVALID_QUALITY_PROFILE",
413
+ /** `quality.policy` sets a gate, severity or budget that is not a legal value. */
414
+ INVALID_QUALITY_POLICY: "E_INVALID_QUALITY_POLICY"
390
415
  };
391
416
  var DEFERRED_TO_COMPILER = /* @__PURE__ */ new Set([
392
417
  ERROR_CODES.UNSUPPORTED_RENDERER_FEATURE
@@ -426,6 +451,34 @@ function validationDiagnostics(errors) {
426
451
  })
427
452
  );
428
453
  }
454
+ function qualityAnalysisDiagnostics(analysis) {
455
+ return analysis.diagnostics.map((diagnostic2) => ({
456
+ source: diagnostic2.source,
457
+ ruleId: diagnostic2.ruleId,
458
+ category: diagnostic2.category,
459
+ certainty: diagnostic2.certainty,
460
+ severity: diagnostic2.severity,
461
+ code: diagnostic2.code,
462
+ message: diagnostic2.message,
463
+ path: diagnostic2.path,
464
+ blocking: diagnostic2.blocking,
465
+ ...diagnostic2.suggestion !== void 0 && {
466
+ suggestion: diagnostic2.suggestion
467
+ },
468
+ ...diagnostic2.context !== void 0 && {
469
+ context: { ...diagnostic2.context }
470
+ },
471
+ ...diagnostic2.relatedPaths !== void 0 && {
472
+ relatedPaths: diagnostic2.relatedPaths
473
+ },
474
+ ...diagnostic2.evidence !== void 0 && {
475
+ evidence: { ...diagnostic2.evidence }
476
+ },
477
+ ...diagnostic2.fixes !== void 0 && {
478
+ fixes: diagnostic2.fixes
479
+ }
480
+ }));
481
+ }
429
482
  function looksLikeValidationErrors(value) {
430
483
  return Array.isArray(value) && value.length > 0 && value.every(
431
484
  (entry) => typeof entry === "object" && entry !== null && typeof entry.message === "string"
@@ -1122,10 +1175,10 @@ var STARTERS = [
1122
1175
  id: "pptx-minimal",
1123
1176
  format: "pptx",
1124
1177
  title: "Minimal presentation",
1125
- description: "The smallest valid .pptx: root, one slide, one title text.",
1178
+ description: "The smallest well-formed .pptx: root with a declared 16:9 canvas, one slide, one title text. The canvas stays: without it the renderer silently falls back to 4:3.",
1126
1179
  document: {
1127
1180
  name: "pptx",
1128
- props: { title: "Untitled deck" },
1181
+ props: { title: "Untitled deck", slideWidth: 13.333, slideHeight: 7.5 },
1129
1182
  children: [
1130
1183
  {
1131
1184
  name: "slide",
@@ -1982,17 +2035,26 @@ function capDiagnostics(diagnostics, limit) {
1982
2035
  if (diagnostics.length <= limit)
1983
2036
  return { kept: diagnostics, truncated: false };
1984
2037
  const ordered = [...diagnostics].sort(
1985
- (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]
2038
+ (a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity] || Number(b.blocking === true) - Number(a.blocking === true)
1986
2039
  );
1987
2040
  return { kept: ordered.slice(0, limit), truncated: true };
1988
2041
  }
2042
+ function ruleErrorDiagnostics(analysis) {
2043
+ return analysis.ruleErrors.map(
2044
+ (entry) => diagnostic(
2045
+ ERROR_CODES.QUALITY_RULE_ERROR,
2046
+ `Quality rule "${entry.ruleId}" failed: ${entry.message}`,
2047
+ { severity: "warning", source: "quality", ruleId: entry.ruleId }
2048
+ )
2049
+ );
2050
+ }
1989
2051
  var DEFAULT_MAX_DIAGNOSTICS = 100;
1990
2052
  function register4(server, deps) {
1991
2053
  server.registerTool(
1992
2054
  "jto_validate",
1993
2055
  {
1994
2056
  title: "Validate a document",
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.",
2057
+ 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. `ok` mirrors generation: schema and semantic errors block; design-quality `W_QUALITY_*` findings advise by default and block only when `quality.policy.gate` requests it. A broken document is a normal result with `ok: false`, never a protocol error.",
1996
2058
  annotations: { readOnlyHint: true, openWorldHint: false },
1997
2059
  inputSchema: S({
1998
2060
  type: "object",
@@ -2008,6 +2070,29 @@ function register4(server, deps) {
2008
2070
  minimum: 1,
2009
2071
  maximum: 1e3,
2010
2072
  description: `Cap on returned diagnostics (default ${DEFAULT_MAX_DIAGNOSTICS}). Errors are kept ahead of warnings when the cap bites.`
2073
+ },
2074
+ quality: {
2075
+ type: "object",
2076
+ description: "Optional design profile plus per-run enforcement policy.",
2077
+ properties: {
2078
+ profile: {
2079
+ type: "object",
2080
+ properties: { id: { type: "string", minLength: 1 } },
2081
+ required: ["id"],
2082
+ additionalProperties: true
2083
+ },
2084
+ policy: {
2085
+ type: "object",
2086
+ properties: {
2087
+ gate: {
2088
+ type: "string",
2089
+ enum: ["none", "error", "warning", "info"]
2090
+ }
2091
+ },
2092
+ additionalProperties: true
2093
+ }
2094
+ },
2095
+ additionalProperties: false
2011
2096
  }
2012
2097
  },
2013
2098
  required: ["format"],
@@ -2038,7 +2123,11 @@ function register4(server, deps) {
2038
2123
  },
2039
2124
  truncated: {
2040
2125
  type: "boolean",
2041
- description: "`diagnostics` was capped by `maxDiagnostics`."
2126
+ description: "`diagnostics` was capped, by `maxDiagnostics` or by the budget the quality policy set."
2127
+ },
2128
+ profileId: {
2129
+ type: "string",
2130
+ description: "The quality profile the design analysis ran under, when one applied."
2042
2131
  }
2043
2132
  })
2044
2133
  )
@@ -2058,24 +2147,52 @@ function register4(server, deps) {
2058
2147
  resolved.document,
2059
2148
  args.renderer
2060
2149
  );
2061
- const all = [
2150
+ let analysis;
2151
+ let qualityOption;
2152
+ if (adapter.analyzeQuality) {
2153
+ try {
2154
+ analysis = await adapter.analyzeQuality(resolved.document, {
2155
+ renderer: args.renderer,
2156
+ quality: args.quality
2157
+ });
2158
+ } catch (error) {
2159
+ const option = result.valid ? void 0 : qualityOptionDiagnostic(error);
2160
+ if (!option) throw error;
2161
+ qualityOption = option;
2162
+ }
2163
+ }
2164
+ const structural = [
2062
2165
  ...validationDiagnostics(result.errors),
2063
- ...unavailable2 ? [unavailable2] : []
2166
+ ...unavailable2 ? [unavailable2] : [],
2167
+ ...qualityOption ? [qualityOption] : []
2168
+ ];
2169
+ const all = [
2170
+ ...structural,
2171
+ ...analysis ? [
2172
+ ...qualityAnalysisDiagnostics(analysis),
2173
+ ...ruleErrorDiagnostics(analysis)
2174
+ ] : []
2064
2175
  ];
2065
2176
  const counts = countDiagnostics(all);
2177
+ const blocked = countDiagnostics(structural).error > 0 || analysis?.blocked === true;
2066
2178
  const { kept, truncated } = capDiagnostics(
2067
2179
  all,
2068
2180
  args.maxDiagnostics ?? DEFAULT_MAX_DIAGNOSTICS
2069
2181
  );
2070
2182
  return {
2071
- ok: counts.error === 0,
2183
+ ok: !blocked,
2072
2184
  diagnostics: kept,
2073
- valid: counts.error === 0,
2185
+ valid: !blocked,
2074
2186
  format: args.format,
2075
2187
  ...args.renderer !== void 0 && { renderer: args.renderer },
2076
2188
  source: sourceSummary(resolved),
2077
2189
  counts,
2078
- truncated
2190
+ // The engine caps its own list under a policy budget, so a report
2191
+ // shortened there would otherwise come back reading complete.
2192
+ truncated: truncated || analysis?.truncated === true,
2193
+ ...analysis?.profileId !== void 0 && {
2194
+ profileId: analysis.profileId
2195
+ }
2079
2196
  };
2080
2197
  })
2081
2198
  )
@@ -5705,6 +5822,7 @@ var RESOURCE_URIS = {
5705
5822
  catalog: "jto://catalog",
5706
5823
  renderers: "jto://renderers",
5707
5824
  themes: "jto://themes",
5825
+ themeValues: "jto://themes/values",
5708
5826
  templates: "jto://templates",
5709
5827
  documentSchema: (format) => `jto://schema/${format}/document`,
5710
5828
  themeSchema: (format) => `jto://schema/${format}/theme`
@@ -5756,7 +5874,7 @@ function register9(server, deps) {
5756
5874
  RESOURCE_URIS.themes,
5757
5875
  {
5758
5876
  title: "Built-in themes",
5759
- description: "Theme names shipped with each format, usable as a document\u2019s props.theme or as the tools\u2019 theme option.",
5877
+ description: "Theme names shipped with each format, usable as a document\u2019s props.theme or as the tools\u2019 theme option. jto://themes/values carries what each name actually looks like.",
5760
5878
  mimeType: JSON_MIME
5761
5879
  },
5762
5880
  async (uri) => {
@@ -5769,6 +5887,33 @@ function register9(server, deps) {
5769
5887
  });
5770
5888
  }
5771
5889
  );
5890
+ server.registerResource(
5891
+ "theme-values",
5892
+ RESOURCE_URIS.themeValues,
5893
+ {
5894
+ title: "Built-in theme values",
5895
+ description: "The palette, fonts, style tables and component defaults behind every built-in theme name \u2014 what a document actually opts into with props.theme. A name alone cannot tell you whether a theme fits the brief; this can.",
5896
+ mimeType: JSON_MIME
5897
+ },
5898
+ async (uri) => {
5899
+ const formats = await Promise.all(
5900
+ FORMAT_NAMES.map(async (format) => {
5901
+ const adapter = deps.getAdapter(format);
5902
+ let themes;
5903
+ try {
5904
+ themes = adapter.getBuiltinThemeValues ? await adapter.getBuiltinThemeValues() : adapter.getBuiltinThemes();
5905
+ } catch {
5906
+ themes = adapter.getBuiltinThemes();
5907
+ }
5908
+ return {
5909
+ format,
5910
+ themes
5911
+ };
5912
+ })
5913
+ );
5914
+ return jsonContents(uri, { formats });
5915
+ }
5916
+ );
5772
5917
  server.registerResource(
5773
5918
  "templates",
5774
5919
  RESOURCE_URIS.templates,
@@ -5817,6 +5962,7 @@ Working rules:
5817
5962
  - Discover before authoring. Call jto_info first, then jto_discover and jto_describe_component (or read the jto:// resources) for the components and renderer ids a format actually supports.
5818
5963
  - Make small edits. With a workspace handle, patch precisely (RFC 6902 over RFC 6901 paths) instead of resending the whole document; without one, change one region at a time.
5819
5964
  - Validate often. Run jto_validate after each edit rather than once at the end; diagnostics are path-addressed, so they map straight back onto the JSON you just changed.
5965
+ - Treat design findings as defects. Schema-valid is not well-designed: jto_validate also lints layout and legibility (W_QUALITY_* \u2014 undeclared slide canvas, text overflowing its box, overcrowded slides, table widths exceeding their section). These never block generation, but they almost always show in the rendered result \u2014 repair them like errors.
5820
5966
  - Preview when the answer is visual. jto_preview renders pages to PNG; use it whenever layout, overflow or fit is in question, not only before finishing.
5821
5967
  - Snapshot before risky changes. jto_workspace_snapshot pins the current revision so a restructuring you cannot cleanly undo is still recoverable.
5822
5968