@nowline/mcp 0.6.0 → 0.7.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/src/server.ts CHANGED
@@ -1,16 +1,21 @@
1
1
  // @nowline/mcp server factory.
2
2
  //
3
- // Creates and configures an McpServer instance with all eight tools and two
4
- // resources (nowline://reference + nowline://examples). The factory is shared
5
- // by the entry-point bin (npx @nowline/mcp) and the CLI's --mcp flag so both
6
- // paths expose an identical surface.
3
+ // Creates and configures an McpServer instance with all tools, resources, and
4
+ // prompts. The factory is shared by the entry-point bin (npx @nowline/mcp)
5
+ // and the CLI's --mcp flag so both paths expose an identical surface.
7
6
  //
8
- // Spec: specs/mcp.md. Plan: export_determinism s8 + s9.
7
+ // Spec: specs/mcp.md.
9
8
 
10
9
  import { promises as fs } from 'node:fs';
11
10
  import * as path from 'node:path';
12
11
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
- import { collectDocumentDiagnostics, createNowlineServices, type NowlineFile } from '@nowline/core';
12
+ import {
13
+ collectDocumentDiagnostics,
14
+ createNowlineServices,
15
+ type NowlineFile,
16
+ parseNowlineJson,
17
+ printNowlineFile,
18
+ } from '@nowline/core';
14
19
  import {
15
20
  type ExportFormat,
16
21
  exportDocument,
@@ -18,9 +23,101 @@ import {
18
23
  type RenderInputs,
19
24
  } from '@nowline/export';
20
25
  import { resolveFonts } from '@nowline/export-core';
26
+ import { buildShareLink } from '@nowline/share-link';
21
27
  import { URI } from 'langium';
22
28
  import { z } from 'zod';
23
- import { EXAMPLES, REFERENCE_MAN_PAGE } from './generated/resources.js';
29
+ import { CAPABILITIES } from './capabilities.js';
30
+ import { CONVERSIONS_GUIDE, EXAMPLES, REFERENCE_MAN_PAGE } from './generated/resources.js';
31
+ import { UI_BUNDLE } from './generated/ui-bundle.js';
32
+ import { registerPrompts } from './prompts.js';
33
+ import {
34
+ CapabilitiesOutputSchema,
35
+ ConvertOutputSchema,
36
+ CreateOutputSchema,
37
+ DeleteOutputSchema,
38
+ ExportOutputSchema,
39
+ ListItemsOutputSchema,
40
+ ListOutputSchema,
41
+ ReadOutputSchema,
42
+ RenderOutputSchema,
43
+ UpdateOutputSchema,
44
+ ValidateOutputSchema,
45
+ } from './schemas.js';
46
+
47
+ // ---- MCP Apps UI (in-chat live preview) -------------------------------------
48
+ //
49
+ // The MCP Apps extension (SEP-1865) lets a tool return an interactive HTML
50
+ // resource the host renders in a sandboxed iframe. We use the embedded-resource
51
+ // form: `render` returns a self-contained text/html resource that inlines the
52
+ // browser preview bundle (UI_BUNDLE, the @nowline/browser + @nowline/preview-
53
+ // shell pipeline) plus the injected source. It is emitted only when the client
54
+ // advertises the UI extension capability or the caller passes `preview: true`,
55
+ // so plain stdio operation is unchanged and non-UI hosts still receive the
56
+ // SVG/PNG content block alongside it (graceful degradation).
57
+
58
+ /** SEP-1865 UI extension capability id; also probed under common short keys. */
59
+ const MCP_APPS_UI_CAPABILITY = 'io.modelcontextprotocol/ui';
60
+ const PREVIEW_UI_URI = 'ui://nowline/preview';
61
+ /** SEP-1865 mandates the text/html;profile=mcp-app media type for UI resources. */
62
+ const PREVIEW_UI_MIME = 'text/html;profile=mcp-app';
63
+
64
+ function clientSupportsAppsUi(server: McpServer): boolean {
65
+ // Extension capabilities are negotiated under `experimental` in SDK 1.29
66
+ // (it does not yet model SEP-1724 extensions as a first-class field), so
67
+ // probe the canonical id plus the short `ui` / `apps` aliases some hosts use.
68
+ const experimental = server.server.getClientCapabilities()?.experimental as
69
+ | Record<string, unknown>
70
+ | undefined;
71
+ if (!experimental) return false;
72
+ return Boolean(experimental[MCP_APPS_UI_CAPABILITY] || experimental.ui || experimental.apps);
73
+ }
74
+
75
+ interface PreviewPayload {
76
+ source: string;
77
+ theme?: string;
78
+ now?: string;
79
+ width?: number;
80
+ locale?: string;
81
+ showLinks?: boolean;
82
+ }
83
+
84
+ function buildPreviewHtml(payload: PreviewPayload): string {
85
+ // The payload (including the .nowline source) is injected as a JSON
86
+ // <script> block rather than interpolated into executable JS, so source
87
+ // text with quotes/backticks can't break out. Escaping `<` as \u003c keeps
88
+ // any embedded "</script>" from closing the block early; JSON.parse in the
89
+ // bundle decodes it back. The bundle injects its own stylesheet at runtime,
90
+ // so only the root-element sizing CSS is inlined here.
91
+ const data = JSON.stringify(payload).replace(/</g, '\\u003c');
92
+ return [
93
+ '<!doctype html>',
94
+ '<html lang="en">',
95
+ '<head>',
96
+ '<meta charset="utf-8" />',
97
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
98
+ '<title>Nowline preview</title>',
99
+ '<style>html,body,#nl-preview-root{margin:0;padding:0;height:100%;width:100%;overflow:hidden;}</style>',
100
+ '</head>',
101
+ '<body>',
102
+ '<div id="nl-preview-root"></div>',
103
+ `<script id="nl-preview-data" type="application/json">${data}</script>`,
104
+ `<script>${UI_BUNDLE}</script>`,
105
+ '</body>',
106
+ '</html>',
107
+ '',
108
+ ].join('\n');
109
+ }
110
+
111
+ function previewResourceBlock(payload: PreviewPayload) {
112
+ return {
113
+ type: 'resource' as const,
114
+ resource: {
115
+ uri: PREVIEW_UI_URI,
116
+ mimeType: PREVIEW_UI_MIME,
117
+ text: buildPreviewHtml(payload),
118
+ },
119
+ };
120
+ }
24
121
 
25
122
  // ---- Diagnostic helpers -----------------------------------------------------
26
123
 
@@ -114,7 +211,6 @@ function createNodeHostEnv(sourcePath: string): HostEnv {
114
211
  return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
115
212
  },
116
213
  async loadWasm(): Promise<ArrayBuffer> {
117
- // Resolve resvg.wasm relative to @nowline/export-png/dist/ at runtime.
118
214
  const { createRequire } = await import('node:module');
119
215
  const req = createRequire(import.meta.url);
120
216
  const entry = req.resolve('@resvg/resvg-wasm');
@@ -163,7 +259,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
163
259
  const allowedRoot = opts.allowedRoot ?? process.cwd();
164
260
  const server = new McpServer({
165
261
  name: opts.name ?? 'nowline',
166
- version: opts.version ?? '0.5.1',
262
+ version: opts.version ?? '0.6.0',
167
263
  });
168
264
 
169
265
  // ---- Resources ----------------------------------------------------------
@@ -199,6 +295,29 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
199
295
  }),
200
296
  );
201
297
 
298
+ server.registerResource(
299
+ 'nowline-conversions',
300
+ 'nowline://conversions',
301
+ {
302
+ description:
303
+ 'LLM-mediated conversion guide: how to translate Mermaid gantt, MS Project, Excel, Google Sheets timeline, and generic CSV into Nowline DSL.',
304
+ mimeType: 'text/plain',
305
+ },
306
+ async () => ({
307
+ contents: [
308
+ {
309
+ uri: 'nowline://conversions',
310
+ text: CONVERSIONS_GUIDE,
311
+ mimeType: 'text/plain',
312
+ },
313
+ ],
314
+ }),
315
+ );
316
+
317
+ // ---- Prompts ------------------------------------------------------------
318
+
319
+ registerPrompts(server);
320
+
202
321
  // ---- validate -----------------------------------------------------------
203
322
 
204
323
  server.registerTool(
@@ -213,14 +332,18 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
213
332
  .optional()
214
333
  .describe('Absolute or relative path to a .nowline file to validate.'),
215
334
  }),
335
+ outputSchema: ValidateOutputSchema,
336
+ annotations: { readOnlyHint: true, idempotentHint: true },
216
337
  },
217
338
  async (args) => {
218
339
  const { source, filePath } = await sourceAndPath(args, allowedRoot);
219
340
  const doc = await buildDocument(source);
220
341
  const diagnostics = collectMcpDiagnostics(doc, filePath);
221
342
  const ok = diagnostics.every((d) => d.severity !== 'error');
343
+ const structured = { ok, diagnostics };
222
344
  return {
223
- content: [{ type: 'text', text: JSON.stringify({ ok, diagnostics }, null, 2) }],
345
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
346
+ structuredContent: structured,
224
347
  };
225
348
  },
226
349
  );
@@ -236,17 +359,16 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
236
359
  .string()
237
360
  .describe('Absolute or relative path to the .nowline file to read.'),
238
361
  }),
362
+ outputSchema: ReadOutputSchema,
363
+ annotations: { readOnlyHint: true, idempotentHint: true },
239
364
  },
240
365
  async (args) => {
241
366
  const abs = resolveAndGuard(args.path, allowedRoot);
242
367
  const source = await fs.readFile(abs, 'utf-8');
368
+ const structured = { path: abs, source };
243
369
  return {
244
- content: [
245
- {
246
- type: 'text',
247
- text: JSON.stringify({ path: abs, source }, null, 2),
248
- },
249
- ],
370
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
371
+ structuredContent: structured,
250
372
  };
251
373
  },
252
374
  );
@@ -262,6 +384,9 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
262
384
  path: z.string().describe('Absolute or relative path to write the .nowline file.'),
263
385
  source: z.string().describe('The .nowline source text to write.'),
264
386
  }),
387
+ outputSchema: CreateOutputSchema,
388
+ // Overwrites silently → destructive; same source always produces same file → idempotent.
389
+ annotations: { destructiveHint: true, idempotentHint: true },
265
390
  },
266
391
  async (args) => {
267
392
  const abs = resolveAndGuard(args.path, allowedRoot);
@@ -281,8 +406,10 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
281
406
  }
282
407
  await fs.mkdir(path.dirname(abs), { recursive: true });
283
408
  await fs.writeFile(abs, args.source, 'utf-8');
409
+ const structured = { ok: true, path: abs };
284
410
  return {
285
- content: [{ type: 'text', text: JSON.stringify({ ok: true, path: abs }, null, 2) }],
411
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
412
+ structuredContent: structured,
286
413
  };
287
414
  },
288
415
  );
@@ -299,6 +426,8 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
299
426
  .describe('Absolute or relative path of the .nowline file to update.'),
300
427
  source: z.string().describe('The new .nowline source text.'),
301
428
  }),
429
+ outputSchema: UpdateOutputSchema,
430
+ annotations: { idempotentHint: true },
302
431
  },
303
432
  async (args) => {
304
433
  const abs = resolveAndGuard(args.path, allowedRoot);
@@ -317,8 +446,10 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
317
446
  };
318
447
  }
319
448
  await fs.writeFile(abs, args.source, 'utf-8');
449
+ const structured = { ok: true, path: abs };
320
450
  return {
321
- content: [{ type: 'text', text: JSON.stringify({ ok: true, path: abs }, null, 2) }],
451
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
452
+ structuredContent: structured,
322
453
  };
323
454
  },
324
455
  );
@@ -334,12 +465,16 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
334
465
  .string()
335
466
  .describe('Absolute or relative path of the .nowline file to delete.'),
336
467
  }),
468
+ outputSchema: DeleteOutputSchema,
469
+ annotations: { destructiveHint: true },
337
470
  },
338
471
  async (args) => {
339
472
  const abs = resolveAndGuard(args.path, allowedRoot);
340
473
  await fs.unlink(abs);
474
+ const structured = { path: abs };
341
475
  return {
342
- content: [{ type: 'text', text: JSON.stringify({ path: abs }, null, 2) }],
476
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
477
+ structuredContent: structured,
343
478
  };
344
479
  },
345
480
  );
@@ -362,13 +497,17 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
362
497
  .optional()
363
498
  .describe('Whether to scan subdirectories. Defaults to false.'),
364
499
  }),
500
+ outputSchema: ListOutputSchema,
501
+ annotations: { readOnlyHint: true, idempotentHint: true },
365
502
  },
366
503
  async (args) => {
367
504
  const dir = args.directory ? resolveAndGuard(args.directory, allowedRoot) : allowedRoot;
368
505
  const recursive = args.recursive ?? false;
369
506
  const paths = await listNowlineFiles(dir, recursive);
507
+ const structured = { paths };
370
508
  return {
371
- content: [{ type: 'text', text: JSON.stringify({ paths }, null, 2) }],
509
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
510
+ structuredContent: structured,
372
511
  };
373
512
  },
374
513
  );
@@ -404,7 +543,21 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
404
543
  .string()
405
544
  .optional()
406
545
  .describe('Write output to this path instead of returning inline.'),
546
+ share: z
547
+ .boolean()
548
+ .optional()
549
+ .describe(
550
+ 'When true, include a shareUrl pointing to https://free.nowline.io/open.',
551
+ ),
552
+ preview: z
553
+ .boolean()
554
+ .optional()
555
+ .describe(
556
+ 'When true, also return an interactive in-chat HTML preview (MCP Apps UI). Auto-enabled when the client advertises MCP Apps UI support.',
557
+ ),
407
558
  }),
559
+ outputSchema: RenderOutputSchema,
560
+ annotations: { readOnlyHint: true, idempotentHint: true },
408
561
  },
409
562
  async (args) => {
410
563
  const { source, filePath } = await sourceAndPath(args, allowedRoot);
@@ -424,22 +577,42 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
424
577
  }
425
578
  const host = createNodeHostEnv(filePath);
426
579
  const bytes = await exportDocument(source, format, inputs, host);
580
+ const shareUrl = args.share
581
+ ? (buildShareLink({ source, share: true }) ?? undefined)
582
+ : undefined;
583
+
584
+ // Optional MCP Apps in-chat preview. Emitted alongside the normal
585
+ // content so non-UI hosts still get the SVG/PNG; the bundle renders
586
+ // the live SVG itself, so the preview is format-agnostic.
587
+ const wantPreview = args.preview === true || clientSupportsAppsUi(server);
588
+ const previewBlocks = wantPreview
589
+ ? [
590
+ previewResourceBlock({
591
+ source,
592
+ theme: args.theme,
593
+ now: args.now,
594
+ width: args.width,
595
+ locale: 'en-US',
596
+ }),
597
+ ]
598
+ : [];
427
599
 
428
600
  if (args.output) {
429
601
  const outAbs = resolveAndGuard(args.output, allowedRoot);
430
602
  await fs.mkdir(path.dirname(outAbs), { recursive: true });
431
603
  await fs.writeFile(outAbs, bytes);
604
+ const structured = { format, path: outAbs, bytes: bytes.byteLength, shareUrl };
432
605
  return {
433
606
  content: [
434
- {
435
- type: 'text',
436
- text: JSON.stringify({ path: outAbs, bytes: bytes.byteLength }),
437
- },
607
+ { type: 'text', text: JSON.stringify(structured, null, 2) },
608
+ ...previewBlocks,
438
609
  ],
610
+ structuredContent: structured,
439
611
  };
440
612
  }
441
613
 
442
614
  if (format === 'png') {
615
+ const structured = { format, bytes: bytes.byteLength, shareUrl };
443
616
  return {
444
617
  content: [
445
618
  {
@@ -447,17 +620,19 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
447
620
  data: Buffer.from(bytes).toString('base64'),
448
621
  mimeType: 'image/png',
449
622
  },
623
+ ...previewBlocks,
450
624
  ],
625
+ structuredContent: structured,
451
626
  };
452
627
  }
628
+ const svgText = new TextDecoder('utf-8').decode(bytes);
629
+ const structured = { format, shareUrl };
453
630
  return {
454
631
  content: [
455
- {
456
- type: 'text',
457
- text: new TextDecoder('utf-8').decode(bytes),
458
- mimeType: 'image/svg+xml',
459
- },
632
+ { type: 'text', text: svgText, mimeType: 'image/svg+xml' },
633
+ ...previewBlocks,
460
634
  ],
635
+ structuredContent: structured,
461
636
  };
462
637
  },
463
638
  );
@@ -495,7 +670,15 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
495
670
  .string()
496
671
  .optional()
497
672
  .describe('MS Project start date override (YYYY-MM-DD).'),
673
+ share: z
674
+ .boolean()
675
+ .optional()
676
+ .describe(
677
+ 'When true, include a shareUrl pointing to https://free.nowline.io/open.',
678
+ ),
498
679
  }),
680
+ outputSchema: ExportOutputSchema,
681
+ annotations: { readOnlyHint: true, idempotentHint: true },
499
682
  },
500
683
  async (args) => {
501
684
  const { source, filePath } = await sourceAndPath(args, allowedRoot);
@@ -518,6 +701,9 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
518
701
  }
519
702
  const host = createNodeHostEnv(filePath);
520
703
  const bytes = await exportDocument(source, format, inputs, host);
704
+ const shareUrl = args.share
705
+ ? (buildShareLink({ source, share: true }) ?? undefined)
706
+ : undefined;
521
707
 
522
708
  const BINARY_FORMATS = new Set<ExportFormat>(['png', 'pdf', 'xlsx']);
523
709
  const isBinary = BINARY_FORMATS.has(format);
@@ -526,13 +712,10 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
526
712
  const outAbs = resolveAndGuard(args.output, allowedRoot);
527
713
  await fs.mkdir(path.dirname(outAbs), { recursive: true });
528
714
  await fs.writeFile(outAbs, bytes);
715
+ const structured = { format, path: outAbs, bytes: bytes.byteLength, shareUrl };
529
716
  return {
530
- content: [
531
- {
532
- type: 'text',
533
- text: JSON.stringify({ path: outAbs, bytes: bytes.byteLength }),
534
- },
535
- ],
717
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
718
+ structuredContent: structured,
536
719
  };
537
720
  }
538
721
 
@@ -542,6 +725,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
542
725
  pdf: 'application/pdf',
543
726
  xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
544
727
  };
728
+ const structured = { format, bytes: bytes.byteLength, shareUrl };
545
729
  return {
546
730
  content: [
547
731
  {
@@ -550,15 +734,198 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
550
734
  mimeType: mimeMap[format] ?? 'application/octet-stream',
551
735
  },
552
736
  ],
737
+ structuredContent: structured,
553
738
  };
554
739
  }
740
+ const text = new TextDecoder('utf-8').decode(bytes);
741
+ const structured = { format, shareUrl };
555
742
  return {
556
- content: [
743
+ content: [{ type: 'text', text }],
744
+ structuredContent: structured,
745
+ };
746
+ },
747
+ );
748
+
749
+ // ---- convert ------------------------------------------------------------
750
+
751
+ server.registerTool(
752
+ 'convert',
753
+ {
754
+ description:
755
+ 'Convert between .nowline source text and its JSON AST representation. `to:json` serializes a .nowline file to the JSON AST; `to:nowline` pretty-prints a JSON AST back to canonical .nowline source.',
756
+ inputSchema: z.object({
757
+ source: z.string().optional().describe('Inline source text to convert.'),
758
+ path: z.string().optional().describe('Path to the source file.'),
759
+ to: z
760
+ .enum(['json', 'nowline'])
761
+ .describe(
762
+ '"json" — serialize .nowline text to JSON AST. "nowline" — pretty-print a JSON AST back to .nowline source.',
763
+ ),
764
+ }),
765
+ outputSchema: ConvertOutputSchema,
766
+ annotations: { readOnlyHint: true, idempotentHint: true },
767
+ },
768
+ async (args) => {
769
+ if (args.to === 'json') {
770
+ const { source, filePath } = await sourceAndPath(args, allowedRoot);
771
+ const host = createNodeHostEnv(filePath);
772
+ const jsonBytes = await exportDocument(
773
+ source,
774
+ 'json',
557
775
  {
558
- type: 'text',
559
- text: new TextDecoder('utf-8').decode(bytes),
776
+ sourcePath: filePath,
777
+ today: todayUtc(),
778
+ locale: 'en-US',
779
+ theme: 'light',
560
780
  },
561
- ],
781
+ host,
782
+ );
783
+ const result = new TextDecoder('utf-8').decode(jsonBytes);
784
+ const structured = { to: 'json' as const, result };
785
+ return {
786
+ content: [{ type: 'text', text: result }],
787
+ structuredContent: structured,
788
+ };
789
+ }
790
+
791
+ // to: 'nowline' — input is a JSON AST string
792
+ const jsonSource =
793
+ args.source ??
794
+ (args.path
795
+ ? await fs.readFile(resolveAndGuard(args.path, allowedRoot), 'utf-8')
796
+ : null);
797
+ if (!jsonSource) {
798
+ throw new Error('At least one of `source` or `path` is required.');
799
+ }
800
+ const { ast } = parseNowlineJson(jsonSource, args.path ?? 'input.json');
801
+ const result = printNowlineFile(ast);
802
+ const structured = { to: 'nowline' as const, result };
803
+ return {
804
+ content: [{ type: 'text', text: result }],
805
+ structuredContent: structured,
806
+ };
807
+ },
808
+ );
809
+
810
+ // ---- capabilities -------------------------------------------------------
811
+
812
+ server.registerTool(
813
+ 'capabilities',
814
+ {
815
+ description:
816
+ 'Return all supported themes, icons, locales, export formats, and template names in a single response.',
817
+ inputSchema: z.object({}),
818
+ outputSchema: CapabilitiesOutputSchema,
819
+ annotations: { readOnlyHint: true, idempotentHint: true },
820
+ },
821
+ async () => {
822
+ const structured = {
823
+ themes: [...CAPABILITIES.themes],
824
+ icons: [...CAPABILITIES.icons],
825
+ locales: [...CAPABILITIES.locales],
826
+ formats: [...CAPABILITIES.formats],
827
+ templates: [...CAPABILITIES.templates],
828
+ };
829
+ return {
830
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
831
+ structuredContent: structured,
832
+ };
833
+ },
834
+ );
835
+
836
+ // ---- list-themes --------------------------------------------------------
837
+
838
+ server.registerTool(
839
+ 'list-themes',
840
+ {
841
+ description: 'List supported color themes: light, dark, grayscale.',
842
+ inputSchema: z.object({}),
843
+ outputSchema: ListItemsOutputSchema,
844
+ annotations: { readOnlyHint: true, idempotentHint: true },
845
+ },
846
+ async () => {
847
+ const structured = { items: [...CAPABILITIES.themes] };
848
+ return {
849
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
850
+ structuredContent: structured,
851
+ };
852
+ },
853
+ );
854
+
855
+ // ---- list-icons ---------------------------------------------------------
856
+
857
+ server.registerTool(
858
+ 'list-icons',
859
+ {
860
+ description:
861
+ 'List built-in capacity-icon names usable in the `capacity-icon:` style property.',
862
+ inputSchema: z.object({}),
863
+ outputSchema: ListItemsOutputSchema,
864
+ annotations: { readOnlyHint: true, idempotentHint: true },
865
+ },
866
+ async () => {
867
+ const structured = { items: [...CAPABILITIES.icons] };
868
+ return {
869
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
870
+ structuredContent: structured,
871
+ };
872
+ },
873
+ );
874
+
875
+ // ---- list-locales -------------------------------------------------------
876
+
877
+ server.registerTool(
878
+ 'list-locales',
879
+ {
880
+ description: 'List supported BCP-47 locale tags.',
881
+ inputSchema: z.object({}),
882
+ outputSchema: ListItemsOutputSchema,
883
+ annotations: { readOnlyHint: true, idempotentHint: true },
884
+ },
885
+ async () => {
886
+ const structured = { items: [...CAPABILITIES.locales] };
887
+ return {
888
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
889
+ structuredContent: structured,
890
+ };
891
+ },
892
+ );
893
+
894
+ // ---- list-formats -------------------------------------------------------
895
+
896
+ server.registerTool(
897
+ 'list-formats',
898
+ {
899
+ description:
900
+ 'List all supported export formats (svg, png, pdf, html, mermaid, xlsx, msproj, json).',
901
+ inputSchema: z.object({}),
902
+ outputSchema: ListItemsOutputSchema,
903
+ annotations: { readOnlyHint: true, idempotentHint: true },
904
+ },
905
+ async () => {
906
+ const structured = { items: [...CAPABILITIES.formats] };
907
+ return {
908
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
909
+ structuredContent: structured,
910
+ };
911
+ },
912
+ );
913
+
914
+ // ---- list-templates -----------------------------------------------------
915
+
916
+ server.registerTool(
917
+ 'list-templates',
918
+ {
919
+ description: 'List built-in template names usable with `nowline --init --template`.',
920
+ inputSchema: z.object({}),
921
+ outputSchema: ListItemsOutputSchema,
922
+ annotations: { readOnlyHint: true, idempotentHint: true },
923
+ },
924
+ async () => {
925
+ const structured = { items: [...CAPABILITIES.templates] };
926
+ return {
927
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
928
+ structuredContent: structured,
562
929
  };
563
930
  },
564
931
  );