@nowline/mcp 0.8.5 → 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/branding.d.ts +3 -3
- package/dist/branding.js +3 -3
- 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 +46 -18
- 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 +7 -13
- 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/prompts.d.ts +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +6 -6
- package/dist/prompts.js.map +1 -1
- 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 +5 -3
- 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 +9 -7
- 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 +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +151 -59
- 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 +24 -17
- package/src/branding.ts +3 -3
- 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 +7 -14
- package/src/options.ts +79 -0
- package/src/prompts.ts +7 -7
- package/src/reference-cheatsheet.ts +27 -2
- package/src/schema-vocab.ts +9 -0
- package/src/schemas.ts +2 -0
- package/src/server.ts +200 -60
- package/src/ui/entry.ts +6 -11
- package/src/ui/payload.ts +26 -0
package/src/server.ts
CHANGED
|
@@ -14,15 +14,20 @@ import {
|
|
|
14
14
|
registerAppResource,
|
|
15
15
|
registerAppTool,
|
|
16
16
|
} from '@modelcontextprotocol/ext-apps/server';
|
|
17
|
-
import { McpServer } from '@modelcontextprotocol/
|
|
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()
|
|
@@ -616,7 +690,8 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
616
690
|
.boolean()
|
|
617
691
|
.optional()
|
|
618
692
|
.describe(
|
|
619
|
-
'When true, force the in-chat MCP Apps preview
|
|
693
|
+
'When true, force the in-chat MCP Apps preview; when false, return inline output even on an MCP Apps host. ' +
|
|
694
|
+
'Omit to follow the host: on MCP Apps hosts the preview auto-renders via _meta.ui.',
|
|
620
695
|
),
|
|
621
696
|
}),
|
|
622
697
|
outputSchema: RenderOutputSchema,
|
|
@@ -630,6 +705,13 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
630
705
|
// Only the filesystem touchpoints (reading the input, writing the
|
|
631
706
|
// output) map to structured NL.MCP.* errors; a kernel render fault
|
|
632
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;
|
|
633
715
|
let source: string;
|
|
634
716
|
let filePath: string;
|
|
635
717
|
try {
|
|
@@ -642,17 +724,19 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
642
724
|
if (!blocked.ok) return blocked.response;
|
|
643
725
|
|
|
644
726
|
const format: ExportFormat = args.format ?? 'svg';
|
|
645
|
-
const today =
|
|
727
|
+
const today = options.today;
|
|
646
728
|
const inputs: RenderInputs = {
|
|
647
729
|
sourcePath: filePath,
|
|
648
730
|
today,
|
|
649
731
|
locale: 'en-US',
|
|
650
|
-
theme:
|
|
651
|
-
width:
|
|
732
|
+
theme: options.theme,
|
|
733
|
+
width: options.width,
|
|
734
|
+
nonWorking: options.nonWorking,
|
|
652
735
|
pngScale: args.scale,
|
|
653
736
|
};
|
|
654
737
|
const host = createNodeHostEnv(filePath);
|
|
655
|
-
|
|
738
|
+
// Explicit true/false wins; undefined follows the host's capabilities.
|
|
739
|
+
const appActive = args.preview ?? clientSupportsAppsUi(server);
|
|
656
740
|
// Skip the full render when the bytes won't be used: apps host with
|
|
657
741
|
// no write-to-disk path and no review attachment requested.
|
|
658
742
|
const needsRender = !appActive || !!args.output || args.review === true;
|
|
@@ -660,32 +744,49 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
660
744
|
const result = await resolveFonts({ headless: true });
|
|
661
745
|
inputs.fonts = { sans: result.sans, mono: result.mono };
|
|
662
746
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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);
|
|
677
770
|
|
|
678
771
|
const previewPayload: PreviewPayload = {
|
|
679
772
|
source,
|
|
680
|
-
theme
|
|
681
|
-
now: args.
|
|
682
|
-
width:
|
|
773
|
+
theme,
|
|
774
|
+
now: previewNow(args, options.today),
|
|
775
|
+
width: options.width,
|
|
776
|
+
nonWorking: options.nonWorking,
|
|
683
777
|
locale: 'en-US',
|
|
684
778
|
};
|
|
685
779
|
|
|
686
780
|
const reviewBlocks =
|
|
687
781
|
args.review === true
|
|
688
|
-
? await buildReviewContentBlocks(
|
|
782
|
+
? await buildReviewContentBlocks(
|
|
783
|
+
blocked.doc,
|
|
784
|
+
source,
|
|
785
|
+
format,
|
|
786
|
+
bytes,
|
|
787
|
+
inputs,
|
|
788
|
+
host,
|
|
789
|
+
)
|
|
689
790
|
: [];
|
|
690
791
|
|
|
691
792
|
const insightHintBlocks =
|
|
@@ -800,10 +901,17 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
800
901
|
'Never pass a virtual or sandbox path such as /home/claude/…, /mnt/user-data/…, or any path ' +
|
|
801
902
|
'outside the configured output folder — those do not exist on the host filesystem.',
|
|
802
903
|
),
|
|
803
|
-
now:
|
|
804
|
-
|
|
904
|
+
now: nowSchema,
|
|
905
|
+
timezone: timezoneSchema,
|
|
906
|
+
theme: themeSchema,
|
|
907
|
+
nonWorking: nonWorkingSchema,
|
|
805
908
|
scale: z.number().optional().describe('PNG scale factor.'),
|
|
806
|
-
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
|
+
),
|
|
807
915
|
orientation: z
|
|
808
916
|
.enum(['portrait', 'landscape', 'auto'])
|
|
809
917
|
.optional()
|
|
@@ -832,6 +940,12 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
832
940
|
// Only the filesystem touchpoints (reading the input, writing the
|
|
833
941
|
// output) map to structured NL.MCP.* errors; a kernel export fault
|
|
834
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
|
+
}
|
|
835
949
|
let source: string;
|
|
836
950
|
let filePath: string;
|
|
837
951
|
try {
|
|
@@ -844,14 +958,14 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
844
958
|
if (!blocked.ok) return blocked.response;
|
|
845
959
|
|
|
846
960
|
const format: ExportFormat = args.format as NonRenderFormat;
|
|
847
|
-
const today = args.now ? new Date(`${args.now}T00:00:00Z`) : todayUtc();
|
|
848
961
|
const inputs: RenderInputs = {
|
|
849
962
|
sourcePath: filePath,
|
|
850
|
-
today,
|
|
963
|
+
today: options.today,
|
|
851
964
|
locale: 'en-US',
|
|
852
|
-
theme:
|
|
965
|
+
theme: options.theme,
|
|
966
|
+
nonWorking: options.nonWorking,
|
|
853
967
|
pngScale: args.scale,
|
|
854
|
-
pageSize:
|
|
968
|
+
pageSize: options.pageSize,
|
|
855
969
|
orientation: args.orientation,
|
|
856
970
|
marginPt: args.marginPt,
|
|
857
971
|
msprojStart: args.msprojStart,
|
|
@@ -861,7 +975,21 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
861
975
|
inputs.fonts = { sans: result.sans, mono: result.mono };
|
|
862
976
|
}
|
|
863
977
|
const host = createNodeHostEnv(filePath);
|
|
864
|
-
|
|
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
|
+
}
|
|
865
993
|
|
|
866
994
|
const BINARY_FORMATS = new Set<ExportFormat>(['png', 'pdf', 'xlsx']);
|
|
867
995
|
const isBinary = BINARY_FORMATS.has(format);
|
|
@@ -1049,12 +1177,15 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1049
1177
|
if (args.to === 'json') {
|
|
1050
1178
|
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
1051
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({});
|
|
1052
1183
|
const jsonBytes = await exportDocument(
|
|
1053
1184
|
source,
|
|
1054
1185
|
'json',
|
|
1055
1186
|
{
|
|
1056
1187
|
sourcePath: filePath,
|
|
1057
|
-
today:
|
|
1188
|
+
today: options.today,
|
|
1058
1189
|
locale: 'en-US',
|
|
1059
1190
|
theme: 'light',
|
|
1060
1191
|
},
|
|
@@ -1121,7 +1252,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1121
1252
|
server.registerTool(
|
|
1122
1253
|
'list-themes',
|
|
1123
1254
|
{
|
|
1124
|
-
description:
|
|
1255
|
+
description: `List supported color themes: ${THEME_NAMES.join(', ')}.`,
|
|
1125
1256
|
inputSchema: z.object({}),
|
|
1126
1257
|
outputSchema: ListItemsOutputSchema,
|
|
1127
1258
|
annotations: readOnlyTool('List Themes'),
|
|
@@ -1300,7 +1431,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1300
1431
|
'schema',
|
|
1301
1432
|
{
|
|
1302
1433
|
description:
|
|
1303
|
-
'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).',
|
|
1304
1435
|
inputSchema: z.object({}),
|
|
1305
1436
|
outputSchema: SchemaOutputSchema,
|
|
1306
1437
|
annotations: readOnlyTool('View Roadmap Schema'),
|
|
@@ -1310,6 +1441,8 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1310
1441
|
directiveKeys: [...SCHEMA_VOCABULARY.directiveKeys],
|
|
1311
1442
|
entityTypes: [...SCHEMA_VOCABULARY.entityTypes],
|
|
1312
1443
|
itemPropertyKeys: [...SCHEMA_VOCABULARY.itemPropertyKeys],
|
|
1444
|
+
configKeywords: [...SCHEMA_VOCABULARY.configKeywords],
|
|
1445
|
+
calendarDeclarationKeys: [...SCHEMA_VOCABULARY.calendarDeclarationKeys],
|
|
1313
1446
|
};
|
|
1314
1447
|
return {
|
|
1315
1448
|
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
@@ -1383,6 +1516,7 @@ function buildExportInlineContentBlocks(
|
|
|
1383
1516
|
}
|
|
1384
1517
|
|
|
1385
1518
|
async function buildReviewContentBlocks(
|
|
1519
|
+
doc: Awaited<ReturnType<typeof buildDocument>>,
|
|
1386
1520
|
source: string,
|
|
1387
1521
|
format: ExportFormat,
|
|
1388
1522
|
artifactBytes: Uint8Array,
|
|
@@ -1413,7 +1547,13 @@ async function buildReviewContentBlocks(
|
|
|
1413
1547
|
const fonts = await resolveFonts({ headless: true });
|
|
1414
1548
|
reviewInputs.fonts = { sans: fonts.sans, mono: fonts.mono };
|
|
1415
1549
|
}
|
|
1416
|
-
|
|
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);
|
|
1417
1557
|
}
|
|
1418
1558
|
|
|
1419
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
|
+
}
|