@nowline/mcp 0.8.6 → 0.9.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/capabilities.d.ts.map +1 -1
- package/dist/capabilities.js +2 -9
- package/dist/capabilities.js.map +1 -1
- package/dist/diagnostics.d.ts +43 -15
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +77 -25
- package/dist/diagnostics.js.map +1 -1
- package/dist/generated/resources.d.ts.map +1 -1
- package/dist/generated/resources.js +15 -3
- package/dist/generated/resources.js.map +1 -1
- package/dist/generated/ui-bundle.d.ts.map +1 -1
- package/dist/generated/ui-bundle.js +2 -2
- package/dist/generated/ui-bundle.js.map +1 -1
- package/dist/http.d.ts +4 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +23 -0
- package/dist/http.js.map +1 -0
- package/dist/index.js +6 -12
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +26 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +55 -0
- package/dist/options.js.map +1 -0
- package/dist/reference-cheatsheet.d.ts +1 -1
- package/dist/reference-cheatsheet.d.ts.map +1 -1
- package/dist/reference-cheatsheet.js +27 -2
- package/dist/reference-cheatsheet.js.map +1 -1
- package/dist/schema-vocab.d.ts +4 -2
- package/dist/schema-vocab.d.ts.map +1 -1
- package/dist/schema-vocab.js +9 -0
- package/dist/schema-vocab.js.map +1 -1
- package/dist/schemas.d.ts +2 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +2 -0
- package/dist/schemas.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +146 -56
- package/dist/server.js.map +1 -1
- package/dist/ui/entry.js +5 -12
- package/dist/ui/entry.js.map +1 -1
- package/dist/ui/payload.d.ts +14 -0
- package/dist/ui/payload.d.ts.map +1 -1
- package/dist/ui/payload.js +23 -0
- package/dist/ui/payload.js.map +1 -1
- package/package.json +21 -16
- package/src/capabilities.ts +4 -2
- package/src/diagnostics.ts +99 -39
- package/src/generated/resources.ts +15 -3
- package/src/generated/ui-bundle.ts +2 -2
- package/src/http.ts +28 -0
- package/src/index.ts +6 -13
- package/src/options.ts +79 -0
- package/src/reference-cheatsheet.ts +27 -2
- package/src/schema-vocab.ts +9 -0
- package/src/schemas.ts +2 -0
- package/src/server.ts +195 -57
- package/src/ui/entry.ts +6 -11
- package/src/ui/payload.ts +26 -0
package/src/server.ts
CHANGED
|
@@ -15,14 +15,19 @@ import {
|
|
|
15
15
|
registerAppTool,
|
|
16
16
|
} from '@modelcontextprotocol/ext-apps/server';
|
|
17
17
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
18
|
+
import type { SurfaceOptions } from '@nowline/config';
|
|
18
19
|
import { type NowlineFile, parseNowlineJson, printNowlineFile } from '@nowline/core';
|
|
19
20
|
import {
|
|
21
|
+
acceptStagedRoadmap,
|
|
20
22
|
type ExportFormat,
|
|
21
23
|
exportDocument,
|
|
24
|
+
exportStaged,
|
|
22
25
|
type HostEnv,
|
|
26
|
+
IncludeResolveError,
|
|
23
27
|
type RenderInputs,
|
|
24
28
|
} from '@nowline/export';
|
|
25
29
|
import { resolveFonts } from '@nowline/export-core';
|
|
30
|
+
import { NON_WORKING_DISPLAYS, THEME_NAMES } from '@nowline/layout';
|
|
26
31
|
import { buildShareLink } from '@nowline/share-link';
|
|
27
32
|
import { z } from 'zod';
|
|
28
33
|
import { NOWLINE_MCP_ICONS } from './branding.js';
|
|
@@ -30,14 +35,20 @@ import { CAPABILITIES } from './capabilities.js';
|
|
|
30
35
|
import {
|
|
31
36
|
buildDocument,
|
|
32
37
|
collectMcpDiagnostics,
|
|
33
|
-
collectMcpLayoutInsights,
|
|
34
38
|
DEFAULT_RENDER_WIDTH,
|
|
35
39
|
diagnosticsErrorBlock,
|
|
40
|
+
diagnosticsErrorResponse,
|
|
36
41
|
handleToolError,
|
|
37
42
|
InputRequiredError,
|
|
43
|
+
includeDiagnosticsResponse,
|
|
38
44
|
LAYOUT_INSIGHT_HINT,
|
|
45
|
+
layoutInsightsFromStage,
|
|
46
|
+
type McpInsight,
|
|
39
47
|
PathOutsideRootError,
|
|
40
48
|
REVIEW_MAX_WIDTH,
|
|
49
|
+
routedResolveDiagnosticsToMcp,
|
|
50
|
+
stageMcpRoadmap,
|
|
51
|
+
stageRoutedDiagnostics,
|
|
41
52
|
toolDescriptionWithSyntax,
|
|
42
53
|
} from './diagnostics.js';
|
|
43
54
|
import {
|
|
@@ -47,6 +58,7 @@ import {
|
|
|
47
58
|
REFERENCE_MAN_PAGE,
|
|
48
59
|
} from './generated/resources.js';
|
|
49
60
|
import { PREVIEW_HTML } from './generated/ui-bundle.js';
|
|
61
|
+
import { type McpResolvedOptions, resolveMcpOptions } from './options.js';
|
|
50
62
|
import { registerPrompts } from './prompts.js';
|
|
51
63
|
import { REFERENCE_CHEATSHEET } from './reference-cheatsheet.js';
|
|
52
64
|
import { SCHEMA_VOCABULARY } from './schema-vocab.js';
|
|
@@ -107,9 +119,24 @@ interface PreviewPayload {
|
|
|
107
119
|
theme?: string;
|
|
108
120
|
now?: string;
|
|
109
121
|
width?: number;
|
|
122
|
+
nonWorking?: 'hide' | 'show';
|
|
110
123
|
locale?: string;
|
|
111
124
|
}
|
|
112
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The preview payload's now-line. An explicit `now` passes through. With only a
|
|
128
|
+
* `timezone`, the payload carries the date that zone resolved to: the preview
|
|
129
|
+
* draws an omitted `now` at the viewer's own date and cannot read the zone.
|
|
130
|
+
*/
|
|
131
|
+
function previewNow(
|
|
132
|
+
args: { now?: string; timezone?: string },
|
|
133
|
+
today: Date | undefined,
|
|
134
|
+
): string | undefined {
|
|
135
|
+
if (args.now !== undefined) return args.now;
|
|
136
|
+
if (args.timezone !== undefined && today) return today.toISOString().slice(0, 10);
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
113
140
|
function leanPreviewBlock(payload: PreviewPayload) {
|
|
114
141
|
return {
|
|
115
142
|
type: 'text' as const,
|
|
@@ -119,6 +146,7 @@ function leanPreviewBlock(payload: PreviewPayload) {
|
|
|
119
146
|
theme: payload.theme,
|
|
120
147
|
now: payload.now,
|
|
121
148
|
width: payload.width,
|
|
149
|
+
nonWorking: payload.nonWorking,
|
|
122
150
|
locale: payload.locale,
|
|
123
151
|
}),
|
|
124
152
|
};
|
|
@@ -185,11 +213,6 @@ function createNodeHostEnv(sourcePath: string): HostEnv {
|
|
|
185
213
|
|
|
186
214
|
// ---- Helpers ----------------------------------------------------------------
|
|
187
215
|
|
|
188
|
-
function todayUtc(): Date {
|
|
189
|
-
const now = new Date();
|
|
190
|
-
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
216
|
function exampleShortName(fullName: string): string {
|
|
194
217
|
return fullName.endsWith('.nowline') ? fullName.slice(0, -'.nowline'.length) : fullName;
|
|
195
218
|
}
|
|
@@ -335,6 +358,43 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
335
358
|
|
|
336
359
|
registerPrompts(server);
|
|
337
360
|
|
|
361
|
+
const nonWorkingSchema = z
|
|
362
|
+
.enum(NON_WORKING_DISPLAYS)
|
|
363
|
+
.optional()
|
|
364
|
+
.describe(
|
|
365
|
+
"How a business calendar's non-working days appear: hide collapses them, " +
|
|
366
|
+
"show draws them as shaded bands. Overrides the file's `default roadmap non-working:` key; " +
|
|
367
|
+
'omit to let the key apply (then hide). The schedule exports (mermaid, msproj, xlsx) are the same either way.',
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
// A string, not an enum, so `Greyscale` and the `greyscale` alias reach
|
|
371
|
+
// `resolveMcpOptions`, which rejects an unknown name with the theme list.
|
|
372
|
+
const themeSchema = z
|
|
373
|
+
.string()
|
|
374
|
+
.optional()
|
|
375
|
+
.describe(
|
|
376
|
+
`Color theme: ${THEME_NAMES.join(', ')} ("greyscale" is accepted). Defaults to light.`,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const nowSchema = z
|
|
380
|
+
.string()
|
|
381
|
+
.regex(/^(\d{4}-\d{2}-\d{2}|-)$/, 'Expected YYYY-MM-DD or "-".')
|
|
382
|
+
.optional()
|
|
383
|
+
.describe(
|
|
384
|
+
"Now-line date as YYYY-MM-DD. Omit to draw it at today's date in `timezone` " +
|
|
385
|
+
"(the server's local zone when `timezone` is omitted too); " +
|
|
386
|
+
'pass "-" to suppress the now-line (same as the CLI\'s `--now -`).',
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const timezoneSchema = z
|
|
390
|
+
.string()
|
|
391
|
+
.optional()
|
|
392
|
+
.describe(
|
|
393
|
+
"Timezone that decides today's date when `now` is omitted (ignored when `now` is a date): " +
|
|
394
|
+
'"local" (the default), "UTC", an ISO 8601 offset ("+05:30", "-07:00", "Z"), ' +
|
|
395
|
+
'or an IANA name ("America/Los_Angeles"). The same forms as the CLI\'s `--timezone`.',
|
|
396
|
+
);
|
|
397
|
+
|
|
338
398
|
// ---- validate -----------------------------------------------------------
|
|
339
399
|
|
|
340
400
|
server.registerTool(
|
|
@@ -349,26 +409,39 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
349
409
|
.string()
|
|
350
410
|
.optional()
|
|
351
411
|
.describe('Absolute or relative path to a .nowline file to validate.'),
|
|
412
|
+
now: nowSchema,
|
|
413
|
+
timezone: timezoneSchema,
|
|
352
414
|
}),
|
|
353
415
|
outputSchema: ValidateOutputSchema,
|
|
354
416
|
annotations: readOnlyTool('Validate Roadmap'),
|
|
355
417
|
},
|
|
356
418
|
async (args) => {
|
|
357
419
|
try {
|
|
420
|
+
const { options } = resolveMcpOptions(args);
|
|
358
421
|
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
359
422
|
const doc = await buildDocument(source);
|
|
360
423
|
const diagnostics = collectMcpDiagnostics(doc, filePath);
|
|
424
|
+
// One stage: the routed (wave-rule) include diagnostics join
|
|
425
|
+
// the validator's, with the file and line they point into
|
|
426
|
+
// (specs/waves.md §6.1), and the insights come from the same
|
|
427
|
+
// layout, logo included.
|
|
428
|
+
let insights: McpInsight[] = [];
|
|
429
|
+
if (diagnostics.every((d) => d.severity !== 'error')) {
|
|
430
|
+
const stage = await stageMcpRoadmap(
|
|
431
|
+
doc,
|
|
432
|
+
source,
|
|
433
|
+
{
|
|
434
|
+
sourcePath: filePath,
|
|
435
|
+
today: options.today,
|
|
436
|
+
locale: 'en-US',
|
|
437
|
+
theme: 'light',
|
|
438
|
+
},
|
|
439
|
+
createNodeHostEnv(filePath),
|
|
440
|
+
);
|
|
441
|
+
diagnostics.push(...stageRoutedDiagnostics(stage));
|
|
442
|
+
if (stage.kind === 'staged') insights = layoutInsightsFromStage(stage);
|
|
443
|
+
}
|
|
361
444
|
const ok = diagnostics.every((d) => d.severity !== 'error');
|
|
362
|
-
const insights = ok
|
|
363
|
-
? await collectMcpLayoutInsights({
|
|
364
|
-
source,
|
|
365
|
-
filePath,
|
|
366
|
-
today: todayUtc(),
|
|
367
|
-
locale: 'en-US',
|
|
368
|
-
readFile: createNodeHostEnv(filePath).readSource,
|
|
369
|
-
doc,
|
|
370
|
-
})
|
|
371
|
-
: [];
|
|
372
445
|
const structured = {
|
|
373
446
|
ok,
|
|
374
447
|
diagnostics,
|
|
@@ -585,15 +658,16 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
585
658
|
.enum(['svg', 'png'])
|
|
586
659
|
.optional()
|
|
587
660
|
.describe('Output format. Defaults to svg.'),
|
|
588
|
-
theme:
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
.string()
|
|
661
|
+
theme: themeSchema,
|
|
662
|
+
now: nowSchema,
|
|
663
|
+
timezone: timezoneSchema,
|
|
664
|
+
width: z
|
|
665
|
+
.number()
|
|
594
666
|
.optional()
|
|
595
|
-
.describe(
|
|
596
|
-
|
|
667
|
+
.describe(
|
|
668
|
+
'Maximum canvas width in px, an integer of at least 320. Unset fits the content; a wider roadmap shrinks its timeline to fit.',
|
|
669
|
+
),
|
|
670
|
+
nonWorking: nonWorkingSchema,
|
|
597
671
|
scale: z
|
|
598
672
|
.number()
|
|
599
673
|
.optional()
|
|
@@ -631,6 +705,13 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
631
705
|
// Only the filesystem touchpoints (reading the input, writing the
|
|
632
706
|
// output) map to structured NL.MCP.* errors; a kernel render fault
|
|
633
707
|
// bubbles unchanged so it is never mislabeled as a path/IO failure.
|
|
708
|
+
let resolved: McpResolvedOptions;
|
|
709
|
+
try {
|
|
710
|
+
resolved = resolveMcpOptions(args);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
return handleToolError(err);
|
|
713
|
+
}
|
|
714
|
+
const { theme, options } = resolved;
|
|
634
715
|
let source: string;
|
|
635
716
|
let filePath: string;
|
|
636
717
|
try {
|
|
@@ -643,13 +724,14 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
643
724
|
if (!blocked.ok) return blocked.response;
|
|
644
725
|
|
|
645
726
|
const format: ExportFormat = args.format ?? 'svg';
|
|
646
|
-
const today =
|
|
727
|
+
const today = options.today;
|
|
647
728
|
const inputs: RenderInputs = {
|
|
648
729
|
sourcePath: filePath,
|
|
649
730
|
today,
|
|
650
731
|
locale: 'en-US',
|
|
651
|
-
theme:
|
|
652
|
-
width:
|
|
732
|
+
theme: options.theme,
|
|
733
|
+
width: options.width,
|
|
734
|
+
nonWorking: options.nonWorking,
|
|
653
735
|
pngScale: args.scale,
|
|
654
736
|
};
|
|
655
737
|
const host = createNodeHostEnv(filePath);
|
|
@@ -662,32 +744,49 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
662
744
|
const result = await resolveFonts({ headless: true });
|
|
663
745
|
inputs.fonts = { sans: result.sans, mono: result.mono };
|
|
664
746
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
747
|
+
// One stage serves the render, the insights and the include check.
|
|
748
|
+
const stage = await stageMcpRoadmap(blocked.doc, source, inputs, host);
|
|
749
|
+
if (stage.kind === 'invalid') {
|
|
750
|
+
return diagnosticsErrorResponse(
|
|
751
|
+
filePath,
|
|
752
|
+
collectMcpDiagnostics(blocked.doc, filePath),
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
// Wave-rule include errors are reported as diagnostics. An uncoded
|
|
756
|
+
// include error throws from a render, as the kernel's does; the
|
|
757
|
+
// preview path, which draws nothing here, keeps the preview it
|
|
758
|
+
// returned before waves existed (specs/waves.md §6.1).
|
|
759
|
+
const routed = stageRoutedDiagnostics(stage);
|
|
760
|
+
if (routed.some((d) => d.severity === 'error')) {
|
|
761
|
+
return includeDiagnosticsResponse(blocked.doc, filePath, routed);
|
|
762
|
+
}
|
|
763
|
+
if (stage.kind === 'include-error' && needsRender) acceptStagedRoadmap(stage, inputs);
|
|
764
|
+
const staged = stage.kind === 'staged' ? stage : undefined;
|
|
765
|
+
const insights = staged ? layoutInsightsFromStage(staged) : [];
|
|
766
|
+
const bytes =
|
|
767
|
+
needsRender && staged
|
|
768
|
+
? await exportStaged(staged, format, inputs, host)
|
|
769
|
+
: new Uint8Array(0);
|
|
679
770
|
|
|
680
771
|
const previewPayload: PreviewPayload = {
|
|
681
772
|
source,
|
|
682
|
-
theme
|
|
683
|
-
now: args.
|
|
684
|
-
width:
|
|
773
|
+
theme,
|
|
774
|
+
now: previewNow(args, options.today),
|
|
775
|
+
width: options.width,
|
|
776
|
+
nonWorking: options.nonWorking,
|
|
685
777
|
locale: 'en-US',
|
|
686
778
|
};
|
|
687
779
|
|
|
688
780
|
const reviewBlocks =
|
|
689
781
|
args.review === true
|
|
690
|
-
? await buildReviewContentBlocks(
|
|
782
|
+
? await buildReviewContentBlocks(
|
|
783
|
+
blocked.doc,
|
|
784
|
+
source,
|
|
785
|
+
format,
|
|
786
|
+
bytes,
|
|
787
|
+
inputs,
|
|
788
|
+
host,
|
|
789
|
+
)
|
|
691
790
|
: [];
|
|
692
791
|
|
|
693
792
|
const insightHintBlocks =
|
|
@@ -802,10 +901,17 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
802
901
|
'Never pass a virtual or sandbox path such as /home/claude/…, /mnt/user-data/…, or any path ' +
|
|
803
902
|
'outside the configured output folder — those do not exist on the host filesystem.',
|
|
804
903
|
),
|
|
805
|
-
now:
|
|
806
|
-
|
|
904
|
+
now: nowSchema,
|
|
905
|
+
timezone: timezoneSchema,
|
|
906
|
+
theme: themeSchema,
|
|
907
|
+
nonWorking: nonWorkingSchema,
|
|
807
908
|
scale: z.number().optional().describe('PNG scale factor.'),
|
|
808
|
-
pageSize: z
|
|
909
|
+
pageSize: z
|
|
910
|
+
.string()
|
|
911
|
+
.optional()
|
|
912
|
+
.describe(
|
|
913
|
+
'PDF page size: a preset name, "content", or <W>x<H><unit> (e.g. 8.5x11in). Validated for every format.',
|
|
914
|
+
),
|
|
809
915
|
orientation: z
|
|
810
916
|
.enum(['portrait', 'landscape', 'auto'])
|
|
811
917
|
.optional()
|
|
@@ -834,6 +940,12 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
834
940
|
// Only the filesystem touchpoints (reading the input, writing the
|
|
835
941
|
// output) map to structured NL.MCP.* errors; a kernel export fault
|
|
836
942
|
// bubbles unchanged so it is never mislabeled as a path/IO failure.
|
|
943
|
+
let options: SurfaceOptions;
|
|
944
|
+
try {
|
|
945
|
+
({ options } = resolveMcpOptions(args));
|
|
946
|
+
} catch (err) {
|
|
947
|
+
return handleToolError(err);
|
|
948
|
+
}
|
|
837
949
|
let source: string;
|
|
838
950
|
let filePath: string;
|
|
839
951
|
try {
|
|
@@ -846,14 +958,14 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
846
958
|
if (!blocked.ok) return blocked.response;
|
|
847
959
|
|
|
848
960
|
const format: ExportFormat = args.format as NonRenderFormat;
|
|
849
|
-
const today = args.now ? new Date(`${args.now}T00:00:00Z`) : todayUtc();
|
|
850
961
|
const inputs: RenderInputs = {
|
|
851
962
|
sourcePath: filePath,
|
|
852
|
-
today,
|
|
963
|
+
today: options.today,
|
|
853
964
|
locale: 'en-US',
|
|
854
|
-
theme:
|
|
965
|
+
theme: options.theme,
|
|
966
|
+
nonWorking: options.nonWorking,
|
|
855
967
|
pngScale: args.scale,
|
|
856
|
-
pageSize:
|
|
968
|
+
pageSize: options.pageSize,
|
|
857
969
|
orientation: args.orientation,
|
|
858
970
|
marginPt: args.marginPt,
|
|
859
971
|
msprojStart: args.msprojStart,
|
|
@@ -863,7 +975,21 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
863
975
|
inputs.fonts = { sans: result.sans, mono: result.mono };
|
|
864
976
|
}
|
|
865
977
|
const host = createNodeHostEnv(filePath);
|
|
866
|
-
|
|
978
|
+
let bytes: Uint8Array;
|
|
979
|
+
try {
|
|
980
|
+
const staged = acceptStagedRoadmap(
|
|
981
|
+
await stageMcpRoadmap(blocked.doc, source, inputs, host),
|
|
982
|
+
inputs,
|
|
983
|
+
);
|
|
984
|
+
bytes = await exportStaged(staged, format, inputs, host);
|
|
985
|
+
} catch (err) {
|
|
986
|
+
if (!(err instanceof IncludeResolveError)) throw err;
|
|
987
|
+
return includeDiagnosticsResponse(
|
|
988
|
+
blocked.doc,
|
|
989
|
+
filePath,
|
|
990
|
+
routedResolveDiagnosticsToMcp(err.diagnostics),
|
|
991
|
+
);
|
|
992
|
+
}
|
|
867
993
|
|
|
868
994
|
const BINARY_FORMATS = new Set<ExportFormat>(['png', 'pdf', 'xlsx']);
|
|
869
995
|
const isBinary = BINARY_FORMATS.has(format);
|
|
@@ -1051,12 +1177,15 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1051
1177
|
if (args.to === 'json') {
|
|
1052
1178
|
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
1053
1179
|
const host = createNodeHostEnv(filePath);
|
|
1180
|
+
// The JSON AST does not depend on today's date, so convert
|
|
1181
|
+
// takes no `now`; the kernel still wants a concrete one.
|
|
1182
|
+
const { options } = resolveMcpOptions({});
|
|
1054
1183
|
const jsonBytes = await exportDocument(
|
|
1055
1184
|
source,
|
|
1056
1185
|
'json',
|
|
1057
1186
|
{
|
|
1058
1187
|
sourcePath: filePath,
|
|
1059
|
-
today:
|
|
1188
|
+
today: options.today,
|
|
1060
1189
|
locale: 'en-US',
|
|
1061
1190
|
theme: 'light',
|
|
1062
1191
|
},
|
|
@@ -1123,7 +1252,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1123
1252
|
server.registerTool(
|
|
1124
1253
|
'list-themes',
|
|
1125
1254
|
{
|
|
1126
|
-
description:
|
|
1255
|
+
description: `List supported color themes: ${THEME_NAMES.join(', ')}.`,
|
|
1127
1256
|
inputSchema: z.object({}),
|
|
1128
1257
|
outputSchema: ListItemsOutputSchema,
|
|
1129
1258
|
annotations: readOnlyTool('List Themes'),
|
|
@@ -1302,7 +1431,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1302
1431
|
'schema',
|
|
1303
1432
|
{
|
|
1304
1433
|
description:
|
|
1305
|
-
'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties).',
|
|
1434
|
+
'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties, config keywords, calendar declaration keys).',
|
|
1306
1435
|
inputSchema: z.object({}),
|
|
1307
1436
|
outputSchema: SchemaOutputSchema,
|
|
1308
1437
|
annotations: readOnlyTool('View Roadmap Schema'),
|
|
@@ -1312,6 +1441,8 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1312
1441
|
directiveKeys: [...SCHEMA_VOCABULARY.directiveKeys],
|
|
1313
1442
|
entityTypes: [...SCHEMA_VOCABULARY.entityTypes],
|
|
1314
1443
|
itemPropertyKeys: [...SCHEMA_VOCABULARY.itemPropertyKeys],
|
|
1444
|
+
configKeywords: [...SCHEMA_VOCABULARY.configKeywords],
|
|
1445
|
+
calendarDeclarationKeys: [...SCHEMA_VOCABULARY.calendarDeclarationKeys],
|
|
1315
1446
|
};
|
|
1316
1447
|
return {
|
|
1317
1448
|
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
@@ -1385,6 +1516,7 @@ function buildExportInlineContentBlocks(
|
|
|
1385
1516
|
}
|
|
1386
1517
|
|
|
1387
1518
|
async function buildReviewContentBlocks(
|
|
1519
|
+
doc: Awaited<ReturnType<typeof buildDocument>>,
|
|
1388
1520
|
source: string,
|
|
1389
1521
|
format: ExportFormat,
|
|
1390
1522
|
artifactBytes: Uint8Array,
|
|
@@ -1415,7 +1547,13 @@ async function buildReviewContentBlocks(
|
|
|
1415
1547
|
const fonts = await resolveFonts({ headless: true });
|
|
1416
1548
|
reviewInputs.fonts = { sans: fonts.sans, mono: fonts.mono };
|
|
1417
1549
|
}
|
|
1418
|
-
|
|
1550
|
+
// A narrower review width is a different layout, so it stages again
|
|
1551
|
+
// (the parsed document is reused).
|
|
1552
|
+
const reviewStage = acceptStagedRoadmap(
|
|
1553
|
+
await stageMcpRoadmap(doc, source, reviewInputs, host),
|
|
1554
|
+
reviewInputs,
|
|
1555
|
+
);
|
|
1556
|
+
inspectionBytes = await exportStaged(reviewStage, 'png', reviewInputs, host);
|
|
1419
1557
|
}
|
|
1420
1558
|
|
|
1421
1559
|
if (format !== 'png' || inspectionBytes.byteLength !== artifactBytes.byteLength) {
|
package/src/ui/entry.ts
CHANGED
|
@@ -25,12 +25,15 @@
|
|
|
25
25
|
declare const __MCP_VERSION__: string;
|
|
26
26
|
|
|
27
27
|
import { App } from '@modelcontextprotocol/ext-apps';
|
|
28
|
+
import { normalizeThemeName } from '@nowline/layout';
|
|
28
29
|
import { type LivePreviewHandle, mountLivePreview } from '@nowline/preview';
|
|
29
30
|
import type { NowOverride, ThemeOverride } from '@nowline/preview-shell';
|
|
30
31
|
import {
|
|
31
32
|
type PreviewPayload,
|
|
32
33
|
parsePreviewFromArguments,
|
|
33
34
|
parsePreviewFromContent,
|
|
35
|
+
toNonWorkingOverride,
|
|
36
|
+
toNowOverride,
|
|
34
37
|
} from './payload.js';
|
|
35
38
|
|
|
36
39
|
function readPayload(): PreviewPayload | undefined {
|
|
@@ -45,16 +48,7 @@ function readPayload(): PreviewPayload | undefined {
|
|
|
45
48
|
|
|
46
49
|
/** Coerce a raw theme token from the payload to the shell's ThemeOverride. */
|
|
47
50
|
function toThemeOverride(theme: string | undefined): ThemeOverride {
|
|
48
|
-
|
|
49
|
-
case 'light':
|
|
50
|
-
case 'dark':
|
|
51
|
-
case 'grayscale':
|
|
52
|
-
case 'greyscale':
|
|
53
|
-
case 'auto':
|
|
54
|
-
return theme;
|
|
55
|
-
default:
|
|
56
|
-
return 'auto';
|
|
57
|
-
}
|
|
51
|
+
return (theme !== undefined ? normalizeThemeName(theme) : undefined) ?? 'auto';
|
|
58
52
|
}
|
|
59
53
|
|
|
60
54
|
let mounted = false;
|
|
@@ -87,7 +81,8 @@ function mountFromPayload(payload: PreviewPayload, app: App): void {
|
|
|
87
81
|
mode: hostTheme(app) ?? 'system',
|
|
88
82
|
initialView: {
|
|
89
83
|
theme: toThemeOverride(payload.theme),
|
|
90
|
-
now: (payload.now
|
|
84
|
+
now: toNowOverride(payload.now) as NowOverride,
|
|
85
|
+
nonWorking: toNonWorkingOverride(payload.nonWorking),
|
|
91
86
|
showLinks: payload.showLinks !== false,
|
|
92
87
|
},
|
|
93
88
|
renderOptions: {
|
package/src/ui/payload.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface PreviewPayload {
|
|
|
13
13
|
theme?: string;
|
|
14
14
|
now?: string;
|
|
15
15
|
width?: number;
|
|
16
|
+
/** The render-time non-working display; absent lets the file's own key apply. */
|
|
17
|
+
nonWorking?: 'hide' | 'show';
|
|
16
18
|
locale?: string;
|
|
17
19
|
showLinks?: boolean;
|
|
18
20
|
showMinimap?: boolean;
|
|
@@ -53,11 +55,35 @@ export function parsePreviewFromArguments(
|
|
|
53
55
|
args: Record<string, unknown> | undefined,
|
|
54
56
|
): PreviewPayload | undefined {
|
|
55
57
|
if (!args || typeof args.source !== 'string') return undefined;
|
|
58
|
+
// A `timezone` without a `now` names a date only the server can resolve;
|
|
59
|
+
// wait for the result payload, which carries it.
|
|
60
|
+
if (typeof args.timezone === 'string' && typeof args.now !== 'string') return undefined;
|
|
56
61
|
return {
|
|
57
62
|
source: args.source,
|
|
58
63
|
theme: typeof args.theme === 'string' ? args.theme : undefined,
|
|
59
64
|
now: typeof args.now === 'string' ? args.now : undefined,
|
|
60
65
|
width: typeof args.width === 'number' ? args.width : undefined,
|
|
66
|
+
nonWorking:
|
|
67
|
+
args.nonWorking === 'hide' || args.nonWorking === 'show' ? args.nonWorking : undefined,
|
|
61
68
|
locale: 'en-US',
|
|
62
69
|
};
|
|
63
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The shell's non-working dropdown value for a payload. A payload with no
|
|
74
|
+
* `nonWorking` maps to `'file'` (no override), so the file's own key applies;
|
|
75
|
+
* anything other than `hide` or `show` is treated as unset.
|
|
76
|
+
*/
|
|
77
|
+
export function toNonWorkingOverride(nonWorking: unknown): 'file' | 'hide' | 'show' {
|
|
78
|
+
return nonWorking === 'hide' || nonWorking === 'show' ? nonWorking : 'file';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The shell's now-line value for a payload's `now`. Omitted means today; `-`
|
|
83
|
+
* (the CLI's `--now -` sentinel, also accepted by the `render` tool) hides the
|
|
84
|
+
* now-line; a date passes through.
|
|
85
|
+
*/
|
|
86
|
+
export function toNowOverride(now: unknown): string {
|
|
87
|
+
if (now === '-') return 'hide';
|
|
88
|
+
return typeof now === 'string' && now.length > 0 ? now : 'today';
|
|
89
|
+
}
|