@nowline/mcp 0.8.0 → 0.8.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/src/prompts.ts CHANGED
@@ -10,6 +10,7 @@ export function registerPrompts(server: McpServer): void {
10
10
  server.registerPrompt(
11
11
  'create-roadmap',
12
12
  {
13
+ title: 'Create Roadmap from Description',
13
14
  description:
14
15
  'Generate a new .nowline roadmap from a plain-English description. Composes the DSL reference and example files so the LLM has full context.',
15
16
  argsSchema: {
@@ -60,6 +61,7 @@ export function registerPrompts(server: McpServer): void {
60
61
  server.registerPrompt(
61
62
  'fix-diagnostics',
62
63
  {
64
+ title: 'Fix Roadmap Diagnostics',
63
65
  description:
64
66
  'Fix validation errors in a .nowline file. Describe the validate→fix→re-validate loop keyed on NL.E#### diagnostic codes.',
65
67
  argsSchema: {
@@ -108,6 +110,7 @@ export function registerPrompts(server: McpServer): void {
108
110
  server.registerPrompt(
109
111
  'convert-to-nowline',
110
112
  {
113
+ title: 'Convert to Nowline',
111
114
  description:
112
115
  'Convert a gantt/timeline from another format (Mermaid gantt, MS Project, Excel, Google Sheets, CSV) into Nowline DSL. Uses the conversion guide resource for format-specific rules.',
113
116
  argsSchema: {
package/src/server.ts CHANGED
@@ -33,7 +33,10 @@ import {
33
33
  collectMcpLayoutInsights,
34
34
  DEFAULT_RENDER_WIDTH,
35
35
  diagnosticsErrorBlock,
36
+ handleToolError,
37
+ InputRequiredError,
36
38
  LAYOUT_INSIGHT_HINT,
39
+ PathOutsideRootError,
37
40
  REVIEW_MAX_WIDTH,
38
41
  toolDescriptionWithSyntax,
39
42
  } from './diagnostics.js';
@@ -120,13 +123,33 @@ function leanPreviewBlock(payload: PreviewPayload) {
120
123
  };
121
124
  }
122
125
 
126
+ // ---- Tool annotation presets (Anthropic Software Directory Policy § 5.E) ---
127
+
128
+ function readOnlyTool(title: string) {
129
+ return {
130
+ title,
131
+ readOnlyHint: true as const,
132
+ idempotentHint: true as const,
133
+ openWorldHint: false as const,
134
+ };
135
+ }
136
+
137
+ function mutatingTool(title: string, opts: { destructiveHint: boolean; idempotentHint?: boolean }) {
138
+ return {
139
+ title,
140
+ destructiveHint: opts.destructiveHint,
141
+ ...(opts.idempotentHint ? { idempotentHint: true as const } : {}),
142
+ openWorldHint: false as const,
143
+ };
144
+ }
145
+
123
146
  // ---- Server factory ---------------------------------------------------------
124
147
 
125
148
  function resolveAndGuard(filePath: string, allowedRoot: string): string {
126
149
  const abs = path.resolve(allowedRoot, filePath);
127
150
  const guard = path.resolve(allowedRoot);
128
151
  if (!abs.startsWith(guard + path.sep) && abs !== guard) {
129
- throw new Error(`Path ${filePath} is outside the allowed root ${allowedRoot}`);
152
+ throw new PathOutsideRootError(filePath, allowedRoot);
130
153
  }
131
154
  return abs;
132
155
  }
@@ -187,7 +210,7 @@ async function sourceAndPath(
187
210
  const source = args.source ?? (await fs.readFile(abs, 'utf-8'));
188
211
  return { source, filePath: abs };
189
212
  }
190
- throw new Error('At least one of `source` or `path` is required.');
213
+ throw new InputRequiredError('At least one of `source` or `path` is required.');
191
214
  }
192
215
 
193
216
  // ---- Server factory ---------------------------------------------------------
@@ -226,6 +249,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
226
249
  'nowline-reference',
227
250
  'nowline://reference',
228
251
  {
252
+ title: 'Roadmap Reference',
229
253
  description:
230
254
  'Full DSL reference (nowline.5 man page): syntax, directives, and examples.',
231
255
  mimeType: 'text/plain',
@@ -241,6 +265,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
241
265
  'nowline-examples',
242
266
  'nowline://examples',
243
267
  {
268
+ title: 'Roadmap Examples',
244
269
  description: 'Canonical example .nowline files from the official examples/ directory.',
245
270
  mimeType: 'text/plain',
246
271
  },
@@ -257,6 +282,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
257
282
  'nowline-conversions',
258
283
  'nowline://conversions',
259
284
  {
285
+ title: 'Conversion Guide',
260
286
  description:
261
287
  'LLM-mediated conversion guide: how to translate Mermaid gantt, MS Project, Excel, Google Sheets timeline, and generic CSV into Nowline DSL.',
262
288
  mimeType: 'text/plain',
@@ -277,6 +303,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
277
303
  'nowline-preview',
278
304
  PREVIEW_UI_URI,
279
305
  {
306
+ title: 'Roadmap Preview',
280
307
  description:
281
308
  'Interactive in-chat roadmap preview (MCP Apps). Hydrates via ontoolresult.',
282
309
  },
@@ -311,33 +338,41 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
311
338
  .describe('Absolute or relative path to a .nowline file to validate.'),
312
339
  }),
313
340
  outputSchema: ValidateOutputSchema,
314
- annotations: { readOnlyHint: true, idempotentHint: true },
341
+ annotations: readOnlyTool('Validate Roadmap'),
315
342
  },
316
343
  async (args) => {
317
- const { source, filePath } = await sourceAndPath(args, allowedRoot);
318
- const doc = await buildDocument(source);
319
- const diagnostics = collectMcpDiagnostics(doc, filePath);
320
- const ok = diagnostics.every((d) => d.severity !== 'error');
321
- const insights = ok
322
- ? await collectMcpLayoutInsights({
323
- source,
324
- filePath,
325
- today: todayUtc(),
326
- locale: 'en-US',
327
- readFile: createNodeHostEnv(filePath).readSource,
328
- doc,
329
- })
330
- : [];
331
- const structured = { ok, diagnostics, ...(insights.length > 0 ? { insights } : {}) };
332
- return {
333
- content: [
334
- { type: 'text', text: JSON.stringify(structured, null, 2) },
335
- ...(insights.length > 0
336
- ? [{ type: 'text' as const, text: LAYOUT_INSIGHT_HINT }]
337
- : []),
338
- ],
339
- structuredContent: structured,
340
- };
344
+ try {
345
+ const { source, filePath } = await sourceAndPath(args, allowedRoot);
346
+ const doc = await buildDocument(source);
347
+ const diagnostics = collectMcpDiagnostics(doc, filePath);
348
+ const ok = diagnostics.every((d) => d.severity !== 'error');
349
+ const insights = ok
350
+ ? await collectMcpLayoutInsights({
351
+ source,
352
+ filePath,
353
+ today: todayUtc(),
354
+ locale: 'en-US',
355
+ readFile: createNodeHostEnv(filePath).readSource,
356
+ doc,
357
+ })
358
+ : [];
359
+ const structured = {
360
+ ok,
361
+ diagnostics,
362
+ ...(insights.length > 0 ? { insights } : {}),
363
+ };
364
+ return {
365
+ content: [
366
+ { type: 'text', text: JSON.stringify(structured, null, 2) },
367
+ ...(insights.length > 0
368
+ ? [{ type: 'text' as const, text: LAYOUT_INSIGHT_HINT }]
369
+ : []),
370
+ ],
371
+ structuredContent: structured,
372
+ };
373
+ } catch (err) {
374
+ return handleToolError(err, args.path);
375
+ }
341
376
  },
342
377
  );
343
378
 
@@ -353,16 +388,20 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
353
388
  .describe('Absolute or relative path to the .nowline file to read.'),
354
389
  }),
355
390
  outputSchema: ReadOutputSchema,
356
- annotations: { readOnlyHint: true, idempotentHint: true },
391
+ annotations: readOnlyTool('Read Roadmap'),
357
392
  },
358
393
  async (args) => {
359
- const abs = resolveAndGuard(args.path, allowedRoot);
360
- const source = await fs.readFile(abs, 'utf-8');
361
- const structured = { path: abs, source };
362
- return {
363
- content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
364
- structuredContent: structured,
365
- };
394
+ try {
395
+ const abs = resolveAndGuard(args.path, allowedRoot);
396
+ const source = await fs.readFile(abs, 'utf-8');
397
+ const structured = { path: abs, source };
398
+ return {
399
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
400
+ structuredContent: structured,
401
+ };
402
+ } catch (err) {
403
+ return handleToolError(err, args.path);
404
+ }
366
405
  },
367
406
  );
368
407
 
@@ -380,19 +419,26 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
380
419
  }),
381
420
  outputSchema: CreateOutputSchema,
382
421
  // Overwrites silently → destructive; same source always produces same file → idempotent.
383
- annotations: { destructiveHint: true, idempotentHint: true },
422
+ annotations: mutatingTool('Create Roadmap', {
423
+ destructiveHint: true,
424
+ idempotentHint: true,
425
+ }),
384
426
  },
385
427
  async (args) => {
386
- const abs = resolveAndGuard(args.path, allowedRoot);
387
- const blocked = await diagnosticsErrorBlock(args.source, abs);
388
- if (!blocked.ok) return blocked.response;
389
- await fs.mkdir(path.dirname(abs), { recursive: true });
390
- await fs.writeFile(abs, args.source, 'utf-8');
391
- const structured = { ok: true, path: abs };
392
- return {
393
- content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
394
- structuredContent: structured,
395
- };
428
+ try {
429
+ const abs = resolveAndGuard(args.path, allowedRoot);
430
+ const blocked = await diagnosticsErrorBlock(args.source, abs);
431
+ if (!blocked.ok) return blocked.response;
432
+ await fs.mkdir(path.dirname(abs), { recursive: true });
433
+ await fs.writeFile(abs, args.source, 'utf-8');
434
+ const structured = { ok: true, path: abs };
435
+ return {
436
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
437
+ structuredContent: structured,
438
+ };
439
+ } catch (err) {
440
+ return handleToolError(err, args.path);
441
+ }
396
442
  },
397
443
  );
398
444
 
@@ -411,18 +457,25 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
411
457
  source: z.string().describe('The new .nowline source text.'),
412
458
  }),
413
459
  outputSchema: UpdateOutputSchema,
414
- annotations: { idempotentHint: true },
460
+ annotations: mutatingTool('Update Roadmap', {
461
+ destructiveHint: true,
462
+ idempotentHint: true,
463
+ }),
415
464
  },
416
465
  async (args) => {
417
- const abs = resolveAndGuard(args.path, allowedRoot);
418
- const blocked = await diagnosticsErrorBlock(args.source, abs);
419
- if (!blocked.ok) return blocked.response;
420
- await fs.writeFile(abs, args.source, 'utf-8');
421
- const structured = { ok: true, path: abs };
422
- return {
423
- content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
424
- structuredContent: structured,
425
- };
466
+ try {
467
+ const abs = resolveAndGuard(args.path, allowedRoot);
468
+ const blocked = await diagnosticsErrorBlock(args.source, abs);
469
+ if (!blocked.ok) return blocked.response;
470
+ await fs.writeFile(abs, args.source, 'utf-8');
471
+ const structured = { ok: true, path: abs };
472
+ return {
473
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
474
+ structuredContent: structured,
475
+ };
476
+ } catch (err) {
477
+ return handleToolError(err, args.path);
478
+ }
426
479
  },
427
480
  );
428
481
 
@@ -438,16 +491,20 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
438
491
  .describe('Absolute or relative path of the .nowline file to delete.'),
439
492
  }),
440
493
  outputSchema: DeleteOutputSchema,
441
- annotations: { destructiveHint: true },
494
+ annotations: mutatingTool('Delete Roadmap', { destructiveHint: true }),
442
495
  },
443
496
  async (args) => {
444
- const abs = resolveAndGuard(args.path, allowedRoot);
445
- await fs.unlink(abs);
446
- const structured = { path: abs };
447
- return {
448
- content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
449
- structuredContent: structured,
450
- };
497
+ try {
498
+ const abs = resolveAndGuard(args.path, allowedRoot);
499
+ await fs.unlink(abs);
500
+ const structured = { path: abs };
501
+ return {
502
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
503
+ structuredContent: structured,
504
+ };
505
+ } catch (err) {
506
+ return handleToolError(err, args.path);
507
+ }
451
508
  },
452
509
  );
453
510
 
@@ -470,17 +527,23 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
470
527
  .describe('Whether to scan subdirectories. Defaults to false.'),
471
528
  }),
472
529
  outputSchema: ListOutputSchema,
473
- annotations: { readOnlyHint: true, idempotentHint: true },
530
+ annotations: readOnlyTool('List Roadmaps'),
474
531
  },
475
532
  async (args) => {
476
- const dir = args.directory ? resolveAndGuard(args.directory, allowedRoot) : allowedRoot;
477
- const recursive = args.recursive ?? false;
478
- const paths = await listNowlineFiles(dir, recursive);
479
- const structured = { paths };
480
- return {
481
- content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
482
- structuredContent: structured,
483
- };
533
+ try {
534
+ const dir = args.directory
535
+ ? resolveAndGuard(args.directory, allowedRoot)
536
+ : allowedRoot;
537
+ const recursive = args.recursive ?? false;
538
+ const paths = await listNowlineFiles(dir, recursive);
539
+ const structured = { paths };
540
+ return {
541
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
542
+ structuredContent: structured,
543
+ };
544
+ } catch (err) {
545
+ return handleToolError(err, args.directory);
546
+ }
484
547
  },
485
548
  );
486
549
 
@@ -549,14 +612,24 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
549
612
  ),
550
613
  }),
551
614
  outputSchema: RenderOutputSchema,
552
- annotations: { readOnlyHint: true, idempotentHint: true },
615
+ annotations: readOnlyTool('Render Roadmap'),
553
616
  _meta: {
554
617
  ui: { resourceUri: PREVIEW_UI_URI },
555
618
  'openai/outputTemplate': PREVIEW_UI_URI,
556
619
  },
557
620
  },
558
621
  async (args) => {
559
- const { source, filePath } = await sourceAndPath(args, allowedRoot);
622
+ // Only the filesystem touchpoints (reading the input, writing the
623
+ // output) map to structured NL.MCP.* errors; a kernel render fault
624
+ // bubbles unchanged so it is never mislabeled as a path/IO failure.
625
+ let source: string;
626
+ let filePath: string;
627
+ try {
628
+ ({ source, filePath } = await sourceAndPath(args, allowedRoot));
629
+ } catch (err) {
630
+ return handleToolError(err, args.path);
631
+ }
632
+
560
633
  const blocked = await diagnosticsErrorBlock(source, filePath);
561
634
  if (!blocked.ok) return blocked.response;
562
635
 
@@ -615,25 +688,29 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
615
688
  const insightsField = insights.length > 0 ? { insights } : {};
616
689
 
617
690
  if (args.output) {
618
- const outAbs = resolveAndGuard(args.output, allowedRoot);
619
- await fs.mkdir(path.dirname(outAbs), { recursive: true });
620
- await fs.writeFile(outAbs, bytes);
621
- const structured = {
622
- format,
623
- path: outAbs,
624
- bytes: bytes.byteLength,
625
- shareUrl,
626
- ...insightsField,
627
- };
628
- return {
629
- content: [
630
- ...(appActive ? [leanPreviewBlock(previewPayload)] : []),
631
- { type: 'text' as const, text: JSON.stringify(structured, null, 2) },
632
- ...insightHintBlocks,
633
- ...reviewBlocks,
634
- ],
635
- structuredContent: structured,
636
- };
691
+ try {
692
+ const outAbs = resolveAndGuard(args.output, allowedRoot);
693
+ await fs.mkdir(path.dirname(outAbs), { recursive: true });
694
+ await fs.writeFile(outAbs, bytes);
695
+ const structured = {
696
+ format,
697
+ path: outAbs,
698
+ bytes: bytes.byteLength,
699
+ shareUrl,
700
+ ...insightsField,
701
+ };
702
+ return {
703
+ content: [
704
+ ...(appActive ? [leanPreviewBlock(previewPayload)] : []),
705
+ { type: 'text' as const, text: JSON.stringify(structured, null, 2) },
706
+ ...insightHintBlocks,
707
+ ...reviewBlocks,
708
+ ],
709
+ structuredContent: structured,
710
+ };
711
+ } catch (err) {
712
+ return handleToolError(err, args.output);
713
+ }
637
714
  }
638
715
 
639
716
  if (appActive) {
@@ -653,7 +730,12 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
653
730
  }
654
731
 
655
732
  if (format === 'png') {
656
- const structured = { format, bytes: bytes.byteLength, shareUrl, ...insightsField };
733
+ const structured = {
734
+ format,
735
+ bytes: bytes.byteLength,
736
+ shareUrl,
737
+ ...insightsField,
738
+ };
657
739
  return {
658
740
  content: [
659
741
  {
@@ -733,10 +815,20 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
733
815
  ),
734
816
  }),
735
817
  outputSchema: ExportOutputSchema,
736
- annotations: { readOnlyHint: true, idempotentHint: true },
818
+ annotations: readOnlyTool('Export Roadmap'),
737
819
  },
738
820
  async (args) => {
739
- const { source, filePath } = await sourceAndPath(args, allowedRoot);
821
+ // Only the filesystem touchpoints (reading the input, writing the
822
+ // output) map to structured NL.MCP.* errors; a kernel export fault
823
+ // bubbles unchanged so it is never mislabeled as a path/IO failure.
824
+ let source: string;
825
+ let filePath: string;
826
+ try {
827
+ ({ source, filePath } = await sourceAndPath(args, allowedRoot));
828
+ } catch (err) {
829
+ return handleToolError(err, args.path);
830
+ }
831
+
740
832
  const blocked = await diagnosticsErrorBlock(source, filePath);
741
833
  if (!blocked.ok) return blocked.response;
742
834
 
@@ -767,14 +859,18 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
767
859
  const isBinary = BINARY_FORMATS.has(format);
768
860
 
769
861
  if (args.output) {
770
- const outAbs = resolveAndGuard(args.output, allowedRoot);
771
- await fs.mkdir(path.dirname(outAbs), { recursive: true });
772
- await fs.writeFile(outAbs, bytes);
773
- const structured = { format, path: outAbs, bytes: bytes.byteLength, shareUrl };
774
- return {
775
- content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
776
- structuredContent: structured,
777
- };
862
+ try {
863
+ const outAbs = resolveAndGuard(args.output, allowedRoot);
864
+ await fs.mkdir(path.dirname(outAbs), { recursive: true });
865
+ await fs.writeFile(outAbs, bytes);
866
+ const structured = { format, path: outAbs, bytes: bytes.byteLength, shareUrl };
867
+ return {
868
+ content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
869
+ structuredContent: structured,
870
+ };
871
+ } catch (err) {
872
+ return handleToolError(err, args.output);
873
+ }
778
874
  }
779
875
 
780
876
  if (isBinary) {
@@ -828,47 +924,51 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
828
924
  ),
829
925
  }),
830
926
  outputSchema: ConvertOutputSchema,
831
- annotations: { readOnlyHint: true, idempotentHint: true },
927
+ annotations: readOnlyTool('Convert Roadmap'),
832
928
  },
833
929
  async (args) => {
834
- if (args.to === 'json') {
835
- const { source, filePath } = await sourceAndPath(args, allowedRoot);
836
- const host = createNodeHostEnv(filePath);
837
- const jsonBytes = await exportDocument(
838
- source,
839
- 'json',
840
- {
841
- sourcePath: filePath,
842
- today: todayUtc(),
843
- locale: 'en-US',
844
- theme: 'light',
845
- },
846
- host,
847
- );
848
- const result = new TextDecoder('utf-8').decode(jsonBytes);
849
- const structured = { to: 'json' as const, result };
930
+ try {
931
+ if (args.to === 'json') {
932
+ const { source, filePath } = await sourceAndPath(args, allowedRoot);
933
+ const host = createNodeHostEnv(filePath);
934
+ const jsonBytes = await exportDocument(
935
+ source,
936
+ 'json',
937
+ {
938
+ sourcePath: filePath,
939
+ today: todayUtc(),
940
+ locale: 'en-US',
941
+ theme: 'light',
942
+ },
943
+ host,
944
+ );
945
+ const result = new TextDecoder('utf-8').decode(jsonBytes);
946
+ const structured = { to: 'json' as const, result };
947
+ return {
948
+ content: [{ type: 'text', text: result }],
949
+ structuredContent: structured,
950
+ };
951
+ }
952
+
953
+ // to: 'nowline' — input is a JSON AST string
954
+ const jsonSource =
955
+ args.source ??
956
+ (args.path
957
+ ? await fs.readFile(resolveAndGuard(args.path, allowedRoot), 'utf-8')
958
+ : null);
959
+ if (!jsonSource) {
960
+ throw new InputRequiredError('At least one of `source` or `path` is required.');
961
+ }
962
+ const { ast } = parseNowlineJson(jsonSource, args.path ?? 'input.json');
963
+ const result = printNowlineFile(ast);
964
+ const structured = { to: 'nowline' as const, result };
850
965
  return {
851
966
  content: [{ type: 'text', text: result }],
852
967
  structuredContent: structured,
853
968
  };
969
+ } catch (err) {
970
+ return handleToolError(err, args.path);
854
971
  }
855
-
856
- // to: 'nowline' — input is a JSON AST string
857
- const jsonSource =
858
- args.source ??
859
- (args.path
860
- ? await fs.readFile(resolveAndGuard(args.path, allowedRoot), 'utf-8')
861
- : null);
862
- if (!jsonSource) {
863
- throw new Error('At least one of `source` or `path` is required.');
864
- }
865
- const { ast } = parseNowlineJson(jsonSource, args.path ?? 'input.json');
866
- const result = printNowlineFile(ast);
867
- const structured = { to: 'nowline' as const, result };
868
- return {
869
- content: [{ type: 'text', text: result }],
870
- structuredContent: structured,
871
- };
872
972
  },
873
973
  );
874
974
 
@@ -881,7 +981,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
881
981
  'Return all supported themes, icons, locales, export formats, and template names in a single response.',
882
982
  inputSchema: z.object({}),
883
983
  outputSchema: CapabilitiesOutputSchema,
884
- annotations: { readOnlyHint: true, idempotentHint: true },
984
+ annotations: readOnlyTool('View Roadmap Capabilities'),
885
985
  },
886
986
  async () => {
887
987
  const structured = {
@@ -906,7 +1006,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
906
1006
  description: 'List supported color themes: light, dark, grayscale.',
907
1007
  inputSchema: z.object({}),
908
1008
  outputSchema: ListItemsOutputSchema,
909
- annotations: { readOnlyHint: true, idempotentHint: true },
1009
+ annotations: readOnlyTool('List Themes'),
910
1010
  },
911
1011
  async () => {
912
1012
  const structured = { items: [...CAPABILITIES.themes] };
@@ -926,7 +1026,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
926
1026
  'List built-in capacity-icon names usable in the `capacity-icon:` style property.',
927
1027
  inputSchema: z.object({}),
928
1028
  outputSchema: ListItemsOutputSchema,
929
- annotations: { readOnlyHint: true, idempotentHint: true },
1029
+ annotations: readOnlyTool('List Icons'),
930
1030
  },
931
1031
  async () => {
932
1032
  const structured = { items: [...CAPABILITIES.icons] };
@@ -945,7 +1045,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
945
1045
  description: 'List supported BCP-47 locale tags.',
946
1046
  inputSchema: z.object({}),
947
1047
  outputSchema: ListItemsOutputSchema,
948
- annotations: { readOnlyHint: true, idempotentHint: true },
1048
+ annotations: readOnlyTool('List Locales'),
949
1049
  },
950
1050
  async () => {
951
1051
  const structured = { items: [...CAPABILITIES.locales] };
@@ -965,7 +1065,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
965
1065
  'List all supported export formats (svg, png, pdf, html, mermaid, xlsx, msproj, json).',
966
1066
  inputSchema: z.object({}),
967
1067
  outputSchema: ListItemsOutputSchema,
968
- annotations: { readOnlyHint: true, idempotentHint: true },
1068
+ annotations: readOnlyTool('List Export Formats'),
969
1069
  },
970
1070
  async () => {
971
1071
  const structured = { items: [...CAPABILITIES.formats] };
@@ -984,7 +1084,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
984
1084
  description: 'List built-in template names usable with `nowline --init --template`.',
985
1085
  inputSchema: z.object({}),
986
1086
  outputSchema: ListItemsOutputSchema,
987
- annotations: { readOnlyHint: true, idempotentHint: true },
1087
+ annotations: readOnlyTool('List Templates'),
988
1088
  },
989
1089
  async () => {
990
1090
  const structured = { items: [...CAPABILITIES.templates] };
@@ -1009,7 +1109,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
1009
1109
  .describe('Reference format. Defaults to condensed.'),
1010
1110
  }),
1011
1111
  outputSchema: ReferenceOutputSchema,
1012
- annotations: { readOnlyHint: true, idempotentHint: true },
1112
+ annotations: readOnlyTool('View Roadmap Reference'),
1013
1113
  },
1014
1114
  async (args) => {
1015
1115
  const format = args.format ?? 'condensed';
@@ -1034,7 +1134,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
1034
1134
  .describe('Example name. Omit for the catalog plus minimal inline.'),
1035
1135
  }),
1036
1136
  outputSchema: ExamplesOutputSchema,
1037
- annotations: { readOnlyHint: true, idempotentHint: true },
1137
+ annotations: readOnlyTool('View Roadmap Examples'),
1038
1138
  },
1039
1139
  async (args) => {
1040
1140
  const exampleNames = EXAMPLES.map((e) => exampleShortName(e.name));
@@ -1085,7 +1185,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
1085
1185
  'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties).',
1086
1186
  inputSchema: z.object({}),
1087
1187
  outputSchema: SchemaOutputSchema,
1088
- annotations: { readOnlyHint: true, idempotentHint: true },
1188
+ annotations: readOnlyTool('View Roadmap Schema'),
1089
1189
  },
1090
1190
  async () => {
1091
1191
  const structured = {