@nowline/mcp 0.8.6 → 0.10.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 +8 -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/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 +4 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +172 -82
- 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/dist/workspace-roots.d.ts +23 -0
- package/dist/workspace-roots.d.ts.map +1 -0
- package/dist/workspace-roots.js +85 -0
- package/dist/workspace-roots.js.map +1 -0
- 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 +8 -14
- 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 +229 -84
- package/src/ui/entry.ts +6 -11
- package/src/ui/payload.ts +26 -0
- package/src/workspace-roots.ts +99 -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';
|
|
@@ -67,6 +79,7 @@ import {
|
|
|
67
79
|
UpdateOutputSchema,
|
|
68
80
|
ValidateOutputSchema,
|
|
69
81
|
} from './schemas.js';
|
|
82
|
+
import { createWorkspaceRoots, isInsideRoots, type WorkspaceRoots } from './workspace-roots.js';
|
|
70
83
|
|
|
71
84
|
// ---- MCP Apps UI (in-chat live preview) -------------------------------------
|
|
72
85
|
//
|
|
@@ -107,9 +120,24 @@ interface PreviewPayload {
|
|
|
107
120
|
theme?: string;
|
|
108
121
|
now?: string;
|
|
109
122
|
width?: number;
|
|
123
|
+
nonWorking?: 'hide' | 'show';
|
|
110
124
|
locale?: string;
|
|
111
125
|
}
|
|
112
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The preview payload's now-line. An explicit `now` passes through. With only a
|
|
129
|
+
* `timezone`, the payload carries the date that zone resolved to: the preview
|
|
130
|
+
* draws an omitted `now` at the viewer's own date and cannot read the zone.
|
|
131
|
+
*/
|
|
132
|
+
function previewNow(
|
|
133
|
+
args: { now?: string; timezone?: string },
|
|
134
|
+
today: Date | undefined,
|
|
135
|
+
): string | undefined {
|
|
136
|
+
if (args.now !== undefined) return args.now;
|
|
137
|
+
if (args.timezone !== undefined && today) return today.toISOString().slice(0, 10);
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
113
141
|
function leanPreviewBlock(payload: PreviewPayload) {
|
|
114
142
|
return {
|
|
115
143
|
type: 'text' as const,
|
|
@@ -119,6 +147,7 @@ function leanPreviewBlock(payload: PreviewPayload) {
|
|
|
119
147
|
theme: payload.theme,
|
|
120
148
|
now: payload.now,
|
|
121
149
|
width: payload.width,
|
|
150
|
+
nonWorking: payload.nonWorking,
|
|
122
151
|
locale: payload.locale,
|
|
123
152
|
}),
|
|
124
153
|
};
|
|
@@ -146,11 +175,10 @@ function mutatingTool(title: string, opts: { destructiveHint: boolean; idempoten
|
|
|
146
175
|
|
|
147
176
|
// ---- Server factory ---------------------------------------------------------
|
|
148
177
|
|
|
149
|
-
function resolveAndGuard(filePath: string,
|
|
150
|
-
const abs = path.resolve(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
throw new PathOutsideRootError(filePath, allowedRoot);
|
|
178
|
+
function resolveAndGuard(filePath: string, roots: WorkspaceRoots): string {
|
|
179
|
+
const abs = path.resolve(roots.primary, filePath);
|
|
180
|
+
if (!isInsideRoots(abs, roots)) {
|
|
181
|
+
throw new PathOutsideRootError(filePath, roots.allowed.join(', '));
|
|
154
182
|
}
|
|
155
183
|
return abs;
|
|
156
184
|
}
|
|
@@ -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
|
}
|
|
@@ -201,13 +224,13 @@ function findExample(name: string): ExampleFile | undefined {
|
|
|
201
224
|
|
|
202
225
|
async function sourceAndPath(
|
|
203
226
|
args: { source?: string; path?: string },
|
|
204
|
-
|
|
227
|
+
roots: WorkspaceRoots,
|
|
205
228
|
): Promise<{ source: string; filePath: string }> {
|
|
206
229
|
if (args.source !== undefined && args.path === undefined) {
|
|
207
|
-
return { source: args.source, filePath: path.join(
|
|
230
|
+
return { source: args.source, filePath: path.join(roots.primary, 'unnamed.nowline') };
|
|
208
231
|
}
|
|
209
232
|
if (args.path !== undefined) {
|
|
210
|
-
const abs = resolveAndGuard(args.path,
|
|
233
|
+
const abs = resolveAndGuard(args.path, roots);
|
|
211
234
|
const source = args.source ?? (await fs.readFile(abs, 'utf-8'));
|
|
212
235
|
return { source, filePath: abs };
|
|
213
236
|
}
|
|
@@ -217,7 +240,10 @@ async function sourceAndPath(
|
|
|
217
240
|
// ---- Server factory ---------------------------------------------------------
|
|
218
241
|
|
|
219
242
|
export interface McpServerOptions {
|
|
220
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* Working directory: all file paths resolve relative to this root. When omitted, the
|
|
245
|
+
* server uses the client's MCP roots if it advertises them, else process.cwd().
|
|
246
|
+
*/
|
|
221
247
|
allowedRoot?: string;
|
|
222
248
|
/** Whether the allowed root was explicitly configured (vs. defaulting to cwd). Controls smart export delivery for binary formats. Defaults to false. */
|
|
223
249
|
rootConfigured?: boolean;
|
|
@@ -228,7 +254,6 @@ export interface McpServerOptions {
|
|
|
228
254
|
}
|
|
229
255
|
|
|
230
256
|
export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
231
|
-
const allowedRoot = opts.allowedRoot ?? process.cwd();
|
|
232
257
|
const rootConfigured = opts.rootConfigured ?? false;
|
|
233
258
|
const server = new McpServer(
|
|
234
259
|
{
|
|
@@ -256,6 +281,8 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
256
281
|
},
|
|
257
282
|
);
|
|
258
283
|
|
|
284
|
+
const workspaceRoots = createWorkspaceRoots(server, opts.allowedRoot);
|
|
285
|
+
|
|
259
286
|
// ---- Resources ----------------------------------------------------------
|
|
260
287
|
|
|
261
288
|
server.registerResource(
|
|
@@ -335,6 +362,43 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
335
362
|
|
|
336
363
|
registerPrompts(server);
|
|
337
364
|
|
|
365
|
+
const nonWorkingSchema = z
|
|
366
|
+
.enum(NON_WORKING_DISPLAYS)
|
|
367
|
+
.optional()
|
|
368
|
+
.describe(
|
|
369
|
+
"How a business calendar's non-working days appear: hide collapses them, " +
|
|
370
|
+
"show draws them as shaded bands. Overrides the file's `default roadmap non-working:` key; " +
|
|
371
|
+
'omit to let the key apply (then hide). The schedule exports (mermaid, msproj, xlsx) are the same either way.',
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// A string, not an enum, so `Greyscale` and the `greyscale` alias reach
|
|
375
|
+
// `resolveMcpOptions`, which rejects an unknown name with the theme list.
|
|
376
|
+
const themeSchema = z
|
|
377
|
+
.string()
|
|
378
|
+
.optional()
|
|
379
|
+
.describe(
|
|
380
|
+
`Color theme: ${THEME_NAMES.join(', ')} ("greyscale" is accepted). Defaults to light.`,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const nowSchema = z
|
|
384
|
+
.string()
|
|
385
|
+
.regex(/^(\d{4}-\d{2}-\d{2}|-)$/, 'Expected YYYY-MM-DD or "-".')
|
|
386
|
+
.optional()
|
|
387
|
+
.describe(
|
|
388
|
+
"Now-line date as YYYY-MM-DD. Omit to draw it at today's date in `timezone` " +
|
|
389
|
+
"(the server's local zone when `timezone` is omitted too); " +
|
|
390
|
+
'pass "-" to suppress the now-line (same as the CLI\'s `--now -`).',
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const timezoneSchema = z
|
|
394
|
+
.string()
|
|
395
|
+
.optional()
|
|
396
|
+
.describe(
|
|
397
|
+
"Timezone that decides today's date when `now` is omitted (ignored when `now` is a date): " +
|
|
398
|
+
'"local" (the default), "UTC", an ISO 8601 offset ("+05:30", "-07:00", "Z"), ' +
|
|
399
|
+
'or an IANA name ("America/Los_Angeles"). The same forms as the CLI\'s `--timezone`.',
|
|
400
|
+
);
|
|
401
|
+
|
|
338
402
|
// ---- validate -----------------------------------------------------------
|
|
339
403
|
|
|
340
404
|
server.registerTool(
|
|
@@ -349,26 +413,39 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
349
413
|
.string()
|
|
350
414
|
.optional()
|
|
351
415
|
.describe('Absolute or relative path to a .nowline file to validate.'),
|
|
416
|
+
now: nowSchema,
|
|
417
|
+
timezone: timezoneSchema,
|
|
352
418
|
}),
|
|
353
419
|
outputSchema: ValidateOutputSchema,
|
|
354
420
|
annotations: readOnlyTool('Validate Roadmap'),
|
|
355
421
|
},
|
|
356
422
|
async (args) => {
|
|
357
423
|
try {
|
|
358
|
-
const {
|
|
424
|
+
const { options } = resolveMcpOptions(args);
|
|
425
|
+
const { source, filePath } = await sourceAndPath(args, await workspaceRoots());
|
|
359
426
|
const doc = await buildDocument(source);
|
|
360
427
|
const diagnostics = collectMcpDiagnostics(doc, filePath);
|
|
428
|
+
// One stage: the routed (wave-rule) include diagnostics join
|
|
429
|
+
// the validator's, with the file and line they point into
|
|
430
|
+
// (specs/waves.md §6.1), and the insights come from the same
|
|
431
|
+
// layout, logo included.
|
|
432
|
+
let insights: McpInsight[] = [];
|
|
433
|
+
if (diagnostics.every((d) => d.severity !== 'error')) {
|
|
434
|
+
const stage = await stageMcpRoadmap(
|
|
435
|
+
doc,
|
|
436
|
+
source,
|
|
437
|
+
{
|
|
438
|
+
sourcePath: filePath,
|
|
439
|
+
today: options.today,
|
|
440
|
+
locale: 'en-US',
|
|
441
|
+
theme: 'light',
|
|
442
|
+
},
|
|
443
|
+
createNodeHostEnv(filePath),
|
|
444
|
+
);
|
|
445
|
+
diagnostics.push(...stageRoutedDiagnostics(stage));
|
|
446
|
+
if (stage.kind === 'staged') insights = layoutInsightsFromStage(stage);
|
|
447
|
+
}
|
|
361
448
|
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
449
|
const structured = {
|
|
373
450
|
ok,
|
|
374
451
|
diagnostics,
|
|
@@ -405,7 +482,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
405
482
|
},
|
|
406
483
|
async (args) => {
|
|
407
484
|
try {
|
|
408
|
-
const abs = resolveAndGuard(args.path,
|
|
485
|
+
const abs = resolveAndGuard(args.path, await workspaceRoots());
|
|
409
486
|
const source = await fs.readFile(abs, 'utf-8');
|
|
410
487
|
const structured = { path: abs, source };
|
|
411
488
|
return {
|
|
@@ -439,7 +516,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
439
516
|
},
|
|
440
517
|
async (args) => {
|
|
441
518
|
try {
|
|
442
|
-
const abs = resolveAndGuard(args.path,
|
|
519
|
+
const abs = resolveAndGuard(args.path, await workspaceRoots());
|
|
443
520
|
const blocked = await diagnosticsErrorBlock(args.source, abs);
|
|
444
521
|
if (!blocked.ok) return blocked.response;
|
|
445
522
|
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
@@ -477,7 +554,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
477
554
|
},
|
|
478
555
|
async (args) => {
|
|
479
556
|
try {
|
|
480
|
-
const abs = resolveAndGuard(args.path,
|
|
557
|
+
const abs = resolveAndGuard(args.path, await workspaceRoots());
|
|
481
558
|
const blocked = await diagnosticsErrorBlock(args.source, abs);
|
|
482
559
|
if (!blocked.ok) return blocked.response;
|
|
483
560
|
await fs.writeFile(abs, args.source, 'utf-8');
|
|
@@ -508,7 +585,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
508
585
|
},
|
|
509
586
|
async (args) => {
|
|
510
587
|
try {
|
|
511
|
-
const abs = resolveAndGuard(args.path,
|
|
588
|
+
const abs = resolveAndGuard(args.path, await workspaceRoots());
|
|
512
589
|
await fs.unlink(abs);
|
|
513
590
|
const structured = { path: abs };
|
|
514
591
|
return {
|
|
@@ -532,7 +609,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
532
609
|
.string()
|
|
533
610
|
.optional()
|
|
534
611
|
.describe(
|
|
535
|
-
|
|
612
|
+
"Directory to scan. Defaults to the project root (--root, else the client's first MCP root, else cwd). Absolute or relative.",
|
|
536
613
|
),
|
|
537
614
|
recursive: z
|
|
538
615
|
.boolean()
|
|
@@ -544,9 +621,8 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
544
621
|
},
|
|
545
622
|
async (args) => {
|
|
546
623
|
try {
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
: allowedRoot;
|
|
624
|
+
const roots = await workspaceRoots();
|
|
625
|
+
const dir = args.directory ? resolveAndGuard(args.directory, roots) : roots.primary;
|
|
550
626
|
const recursive = args.recursive ?? false;
|
|
551
627
|
const paths = await listNowlineFiles(dir, recursive);
|
|
552
628
|
const structured = { paths };
|
|
@@ -585,15 +661,16 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
585
661
|
.enum(['svg', 'png'])
|
|
586
662
|
.optional()
|
|
587
663
|
.describe('Output format. Defaults to svg.'),
|
|
588
|
-
theme:
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
.string()
|
|
664
|
+
theme: themeSchema,
|
|
665
|
+
now: nowSchema,
|
|
666
|
+
timezone: timezoneSchema,
|
|
667
|
+
width: z
|
|
668
|
+
.number()
|
|
594
669
|
.optional()
|
|
595
|
-
.describe(
|
|
596
|
-
|
|
670
|
+
.describe(
|
|
671
|
+
'Maximum canvas width in px, an integer of at least 320. Unset fits the content; a wider roadmap shrinks its timeline to fit.',
|
|
672
|
+
),
|
|
673
|
+
nonWorking: nonWorkingSchema,
|
|
597
674
|
scale: z
|
|
598
675
|
.number()
|
|
599
676
|
.optional()
|
|
@@ -631,10 +708,17 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
631
708
|
// Only the filesystem touchpoints (reading the input, writing the
|
|
632
709
|
// output) map to structured NL.MCP.* errors; a kernel render fault
|
|
633
710
|
// bubbles unchanged so it is never mislabeled as a path/IO failure.
|
|
711
|
+
let resolved: McpResolvedOptions;
|
|
712
|
+
try {
|
|
713
|
+
resolved = resolveMcpOptions(args);
|
|
714
|
+
} catch (err) {
|
|
715
|
+
return handleToolError(err);
|
|
716
|
+
}
|
|
717
|
+
const { theme, options } = resolved;
|
|
634
718
|
let source: string;
|
|
635
719
|
let filePath: string;
|
|
636
720
|
try {
|
|
637
|
-
({ source, filePath } = await sourceAndPath(args,
|
|
721
|
+
({ source, filePath } = await sourceAndPath(args, await workspaceRoots()));
|
|
638
722
|
} catch (err) {
|
|
639
723
|
return handleToolError(err, args.path);
|
|
640
724
|
}
|
|
@@ -643,13 +727,14 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
643
727
|
if (!blocked.ok) return blocked.response;
|
|
644
728
|
|
|
645
729
|
const format: ExportFormat = args.format ?? 'svg';
|
|
646
|
-
const today =
|
|
730
|
+
const today = options.today;
|
|
647
731
|
const inputs: RenderInputs = {
|
|
648
732
|
sourcePath: filePath,
|
|
649
733
|
today,
|
|
650
734
|
locale: 'en-US',
|
|
651
|
-
theme:
|
|
652
|
-
width:
|
|
735
|
+
theme: options.theme,
|
|
736
|
+
width: options.width,
|
|
737
|
+
nonWorking: options.nonWorking,
|
|
653
738
|
pngScale: args.scale,
|
|
654
739
|
};
|
|
655
740
|
const host = createNodeHostEnv(filePath);
|
|
@@ -662,32 +747,49 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
662
747
|
const result = await resolveFonts({ headless: true });
|
|
663
748
|
inputs.fonts = { sans: result.sans, mono: result.mono };
|
|
664
749
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
750
|
+
// One stage serves the render, the insights and the include check.
|
|
751
|
+
const stage = await stageMcpRoadmap(blocked.doc, source, inputs, host);
|
|
752
|
+
if (stage.kind === 'invalid') {
|
|
753
|
+
return diagnosticsErrorResponse(
|
|
754
|
+
filePath,
|
|
755
|
+
collectMcpDiagnostics(blocked.doc, filePath),
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
// Wave-rule include errors are reported as diagnostics. An uncoded
|
|
759
|
+
// include error throws from a render, as the kernel's does; the
|
|
760
|
+
// preview path, which draws nothing here, keeps the preview it
|
|
761
|
+
// returned before waves existed (specs/waves.md §6.1).
|
|
762
|
+
const routed = stageRoutedDiagnostics(stage);
|
|
763
|
+
if (routed.some((d) => d.severity === 'error')) {
|
|
764
|
+
return includeDiagnosticsResponse(blocked.doc, filePath, routed);
|
|
765
|
+
}
|
|
766
|
+
if (stage.kind === 'include-error' && needsRender) acceptStagedRoadmap(stage, inputs);
|
|
767
|
+
const staged = stage.kind === 'staged' ? stage : undefined;
|
|
768
|
+
const insights = staged ? layoutInsightsFromStage(staged) : [];
|
|
769
|
+
const bytes =
|
|
770
|
+
needsRender && staged
|
|
771
|
+
? await exportStaged(staged, format, inputs, host)
|
|
772
|
+
: new Uint8Array(0);
|
|
679
773
|
|
|
680
774
|
const previewPayload: PreviewPayload = {
|
|
681
775
|
source,
|
|
682
|
-
theme
|
|
683
|
-
now: args.
|
|
684
|
-
width:
|
|
776
|
+
theme,
|
|
777
|
+
now: previewNow(args, options.today),
|
|
778
|
+
width: options.width,
|
|
779
|
+
nonWorking: options.nonWorking,
|
|
685
780
|
locale: 'en-US',
|
|
686
781
|
};
|
|
687
782
|
|
|
688
783
|
const reviewBlocks =
|
|
689
784
|
args.review === true
|
|
690
|
-
? await buildReviewContentBlocks(
|
|
785
|
+
? await buildReviewContentBlocks(
|
|
786
|
+
blocked.doc,
|
|
787
|
+
source,
|
|
788
|
+
format,
|
|
789
|
+
bytes,
|
|
790
|
+
inputs,
|
|
791
|
+
host,
|
|
792
|
+
)
|
|
691
793
|
: [];
|
|
692
794
|
|
|
693
795
|
const insightHintBlocks =
|
|
@@ -696,7 +798,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
696
798
|
|
|
697
799
|
if (args.output) {
|
|
698
800
|
try {
|
|
699
|
-
const outAbs = resolveAndGuard(args.output,
|
|
801
|
+
const outAbs = resolveAndGuard(args.output, await workspaceRoots());
|
|
700
802
|
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
701
803
|
await fs.writeFile(outAbs, bytes);
|
|
702
804
|
const structured = {
|
|
@@ -802,10 +904,17 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
802
904
|
'Never pass a virtual or sandbox path such as /home/claude/…, /mnt/user-data/…, or any path ' +
|
|
803
905
|
'outside the configured output folder — those do not exist on the host filesystem.',
|
|
804
906
|
),
|
|
805
|
-
now:
|
|
806
|
-
|
|
907
|
+
now: nowSchema,
|
|
908
|
+
timezone: timezoneSchema,
|
|
909
|
+
theme: themeSchema,
|
|
910
|
+
nonWorking: nonWorkingSchema,
|
|
807
911
|
scale: z.number().optional().describe('PNG scale factor.'),
|
|
808
|
-
pageSize: z
|
|
912
|
+
pageSize: z
|
|
913
|
+
.string()
|
|
914
|
+
.optional()
|
|
915
|
+
.describe(
|
|
916
|
+
'PDF page size: a preset name, "content", or <W>x<H><unit> (e.g. 8.5x11in). Validated for every format.',
|
|
917
|
+
),
|
|
809
918
|
orientation: z
|
|
810
919
|
.enum(['portrait', 'landscape', 'auto'])
|
|
811
920
|
.optional()
|
|
@@ -834,10 +943,16 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
834
943
|
// Only the filesystem touchpoints (reading the input, writing the
|
|
835
944
|
// output) map to structured NL.MCP.* errors; a kernel export fault
|
|
836
945
|
// bubbles unchanged so it is never mislabeled as a path/IO failure.
|
|
946
|
+
let options: SurfaceOptions;
|
|
947
|
+
try {
|
|
948
|
+
({ options } = resolveMcpOptions(args));
|
|
949
|
+
} catch (err) {
|
|
950
|
+
return handleToolError(err);
|
|
951
|
+
}
|
|
837
952
|
let source: string;
|
|
838
953
|
let filePath: string;
|
|
839
954
|
try {
|
|
840
|
-
({ source, filePath } = await sourceAndPath(args,
|
|
955
|
+
({ source, filePath } = await sourceAndPath(args, await workspaceRoots()));
|
|
841
956
|
} catch (err) {
|
|
842
957
|
return handleToolError(err, args.path);
|
|
843
958
|
}
|
|
@@ -846,14 +961,14 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
846
961
|
if (!blocked.ok) return blocked.response;
|
|
847
962
|
|
|
848
963
|
const format: ExportFormat = args.format as NonRenderFormat;
|
|
849
|
-
const today = args.now ? new Date(`${args.now}T00:00:00Z`) : todayUtc();
|
|
850
964
|
const inputs: RenderInputs = {
|
|
851
965
|
sourcePath: filePath,
|
|
852
|
-
today,
|
|
966
|
+
today: options.today,
|
|
853
967
|
locale: 'en-US',
|
|
854
|
-
theme:
|
|
968
|
+
theme: options.theme,
|
|
969
|
+
nonWorking: options.nonWorking,
|
|
855
970
|
pngScale: args.scale,
|
|
856
|
-
pageSize:
|
|
971
|
+
pageSize: options.pageSize,
|
|
857
972
|
orientation: args.orientation,
|
|
858
973
|
marginPt: args.marginPt,
|
|
859
974
|
msprojStart: args.msprojStart,
|
|
@@ -863,7 +978,21 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
863
978
|
inputs.fonts = { sans: result.sans, mono: result.mono };
|
|
864
979
|
}
|
|
865
980
|
const host = createNodeHostEnv(filePath);
|
|
866
|
-
|
|
981
|
+
let bytes: Uint8Array;
|
|
982
|
+
try {
|
|
983
|
+
const staged = acceptStagedRoadmap(
|
|
984
|
+
await stageMcpRoadmap(blocked.doc, source, inputs, host),
|
|
985
|
+
inputs,
|
|
986
|
+
);
|
|
987
|
+
bytes = await exportStaged(staged, format, inputs, host);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
if (!(err instanceof IncludeResolveError)) throw err;
|
|
990
|
+
return includeDiagnosticsResponse(
|
|
991
|
+
blocked.doc,
|
|
992
|
+
filePath,
|
|
993
|
+
routedResolveDiagnosticsToMcp(err.diagnostics),
|
|
994
|
+
);
|
|
995
|
+
}
|
|
867
996
|
|
|
868
997
|
const BINARY_FORMATS = new Set<ExportFormat>(['png', 'pdf', 'xlsx']);
|
|
869
998
|
const isBinary = BINARY_FORMATS.has(format);
|
|
@@ -917,11 +1046,12 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
917
1046
|
}
|
|
918
1047
|
|
|
919
1048
|
// Write to disk if needed.
|
|
920
|
-
const
|
|
1049
|
+
const roots = await workspaceRoots();
|
|
1050
|
+
const outTarget = args.output ?? path.join(roots.primary, defaultFilename);
|
|
921
1051
|
let writtenPath: string | undefined;
|
|
922
1052
|
if (doWrite) {
|
|
923
1053
|
try {
|
|
924
|
-
const outAbs = resolveAndGuard(outTarget,
|
|
1054
|
+
const outAbs = resolveAndGuard(outTarget, roots);
|
|
925
1055
|
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
926
1056
|
await fs.writeFile(outAbs, bytes);
|
|
927
1057
|
writtenPath = outAbs;
|
|
@@ -1000,7 +1130,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1000
1130
|
let source: string;
|
|
1001
1131
|
let filePath: string;
|
|
1002
1132
|
try {
|
|
1003
|
-
({ source, filePath } = await sourceAndPath(args,
|
|
1133
|
+
({ source, filePath } = await sourceAndPath(args, await workspaceRoots()));
|
|
1004
1134
|
} catch (err) {
|
|
1005
1135
|
return handleToolError(err, args.path);
|
|
1006
1136
|
}
|
|
@@ -1049,14 +1179,17 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1049
1179
|
async (args) => {
|
|
1050
1180
|
try {
|
|
1051
1181
|
if (args.to === 'json') {
|
|
1052
|
-
const { source, filePath } = await sourceAndPath(args,
|
|
1182
|
+
const { source, filePath } = await sourceAndPath(args, await workspaceRoots());
|
|
1053
1183
|
const host = createNodeHostEnv(filePath);
|
|
1184
|
+
// The JSON AST does not depend on today's date, so convert
|
|
1185
|
+
// takes no `now`; the kernel still wants a concrete one.
|
|
1186
|
+
const { options } = resolveMcpOptions({});
|
|
1054
1187
|
const jsonBytes = await exportDocument(
|
|
1055
1188
|
source,
|
|
1056
1189
|
'json',
|
|
1057
1190
|
{
|
|
1058
1191
|
sourcePath: filePath,
|
|
1059
|
-
today:
|
|
1192
|
+
today: options.today,
|
|
1060
1193
|
locale: 'en-US',
|
|
1061
1194
|
theme: 'light',
|
|
1062
1195
|
},
|
|
@@ -1074,7 +1207,10 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1074
1207
|
const jsonSource =
|
|
1075
1208
|
args.source ??
|
|
1076
1209
|
(args.path
|
|
1077
|
-
? await fs.readFile(
|
|
1210
|
+
? await fs.readFile(
|
|
1211
|
+
resolveAndGuard(args.path, await workspaceRoots()),
|
|
1212
|
+
'utf-8',
|
|
1213
|
+
)
|
|
1078
1214
|
: null);
|
|
1079
1215
|
if (!jsonSource) {
|
|
1080
1216
|
throw new InputRequiredError('At least one of `source` or `path` is required.');
|
|
@@ -1123,7 +1259,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1123
1259
|
server.registerTool(
|
|
1124
1260
|
'list-themes',
|
|
1125
1261
|
{
|
|
1126
|
-
description:
|
|
1262
|
+
description: `List supported color themes: ${THEME_NAMES.join(', ')}.`,
|
|
1127
1263
|
inputSchema: z.object({}),
|
|
1128
1264
|
outputSchema: ListItemsOutputSchema,
|
|
1129
1265
|
annotations: readOnlyTool('List Themes'),
|
|
@@ -1302,7 +1438,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1302
1438
|
'schema',
|
|
1303
1439
|
{
|
|
1304
1440
|
description:
|
|
1305
|
-
'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties).',
|
|
1441
|
+
'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties, config keywords, calendar declaration keys).',
|
|
1306
1442
|
inputSchema: z.object({}),
|
|
1307
1443
|
outputSchema: SchemaOutputSchema,
|
|
1308
1444
|
annotations: readOnlyTool('View Roadmap Schema'),
|
|
@@ -1312,6 +1448,8 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
1312
1448
|
directiveKeys: [...SCHEMA_VOCABULARY.directiveKeys],
|
|
1313
1449
|
entityTypes: [...SCHEMA_VOCABULARY.entityTypes],
|
|
1314
1450
|
itemPropertyKeys: [...SCHEMA_VOCABULARY.itemPropertyKeys],
|
|
1451
|
+
configKeywords: [...SCHEMA_VOCABULARY.configKeywords],
|
|
1452
|
+
calendarDeclarationKeys: [...SCHEMA_VOCABULARY.calendarDeclarationKeys],
|
|
1315
1453
|
};
|
|
1316
1454
|
return {
|
|
1317
1455
|
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
@@ -1385,6 +1523,7 @@ function buildExportInlineContentBlocks(
|
|
|
1385
1523
|
}
|
|
1386
1524
|
|
|
1387
1525
|
async function buildReviewContentBlocks(
|
|
1526
|
+
doc: Awaited<ReturnType<typeof buildDocument>>,
|
|
1388
1527
|
source: string,
|
|
1389
1528
|
format: ExportFormat,
|
|
1390
1529
|
artifactBytes: Uint8Array,
|
|
@@ -1415,7 +1554,13 @@ async function buildReviewContentBlocks(
|
|
|
1415
1554
|
const fonts = await resolveFonts({ headless: true });
|
|
1416
1555
|
reviewInputs.fonts = { sans: fonts.sans, mono: fonts.mono };
|
|
1417
1556
|
}
|
|
1418
|
-
|
|
1557
|
+
// A narrower review width is a different layout, so it stages again
|
|
1558
|
+
// (the parsed document is reused).
|
|
1559
|
+
const reviewStage = acceptStagedRoadmap(
|
|
1560
|
+
await stageMcpRoadmap(doc, source, reviewInputs, host),
|
|
1561
|
+
reviewInputs,
|
|
1562
|
+
);
|
|
1563
|
+
inspectionBytes = await exportStaged(reviewStage, 'png', reviewInputs, host);
|
|
1419
1564
|
}
|
|
1420
1565
|
|
|
1421
1566
|
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: {
|