@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/dist/server.js
CHANGED
|
@@ -10,19 +10,22 @@ import * as path from 'node:path';
|
|
|
10
10
|
import { getUiCapability, RESOURCE_MIME_TYPE, registerAppResource, registerAppTool, } from '@modelcontextprotocol/ext-apps/server';
|
|
11
11
|
import { McpServer } from '@modelcontextprotocol/server';
|
|
12
12
|
import { parseNowlineJson, printNowlineFile } from '@nowline/core';
|
|
13
|
-
import { exportDocument, } from '@nowline/export';
|
|
13
|
+
import { acceptStagedRoadmap, exportDocument, exportStaged, IncludeResolveError, } from '@nowline/export';
|
|
14
14
|
import { resolveFonts } from '@nowline/export-core';
|
|
15
|
+
import { NON_WORKING_DISPLAYS, THEME_NAMES } from '@nowline/layout';
|
|
15
16
|
import { buildShareLink } from '@nowline/share-link';
|
|
16
17
|
import { z } from 'zod';
|
|
17
18
|
import { NOWLINE_MCP_ICONS } from './branding.js';
|
|
18
19
|
import { CAPABILITIES } from './capabilities.js';
|
|
19
|
-
import { buildDocument, collectMcpDiagnostics,
|
|
20
|
+
import { buildDocument, collectMcpDiagnostics, DEFAULT_RENDER_WIDTH, diagnosticsErrorBlock, diagnosticsErrorResponse, handleToolError, InputRequiredError, includeDiagnosticsResponse, LAYOUT_INSIGHT_HINT, layoutInsightsFromStage, PathOutsideRootError, REVIEW_MAX_WIDTH, routedResolveDiagnosticsToMcp, stageMcpRoadmap, stageRoutedDiagnostics, toolDescriptionWithSyntax, } from './diagnostics.js';
|
|
20
21
|
import { CONVERSIONS_GUIDE, EXAMPLES, REFERENCE_MAN_PAGE, } from './generated/resources.js';
|
|
21
22
|
import { PREVIEW_HTML } from './generated/ui-bundle.js';
|
|
23
|
+
import { resolveMcpOptions } from './options.js';
|
|
22
24
|
import { registerPrompts } from './prompts.js';
|
|
23
25
|
import { REFERENCE_CHEATSHEET } from './reference-cheatsheet.js';
|
|
24
26
|
import { SCHEMA_VOCABULARY } from './schema-vocab.js';
|
|
25
27
|
import { CapabilitiesOutputSchema, ConvertOutputSchema, CreateOutputSchema, DeleteOutputSchema, ExamplesOutputSchema, ExportOutputSchema, ListItemsOutputSchema, ListOutputSchema, ReadOutputSchema, ReferenceOutputSchema, RenderOutputSchema, SchemaOutputSchema, ShareOutputSchema, UpdateOutputSchema, ValidateOutputSchema, } from './schemas.js';
|
|
28
|
+
import { createWorkspaceRoots, isInsideRoots } from './workspace-roots.js';
|
|
26
29
|
// ---- MCP Apps UI (in-chat live preview) -------------------------------------
|
|
27
30
|
//
|
|
28
31
|
// Official MCP Apps model (SEP-1865): a pre-declared ui:// resource serves
|
|
@@ -46,6 +49,18 @@ function clientSupportsAppsUi(server) {
|
|
|
46
49
|
const buckets = [caps.extensions, caps.experimental].filter((bucket) => Boolean(bucket));
|
|
47
50
|
return buckets.some((bucket) => Boolean(bucket[MCP_APPS_UI_CAPABILITY] || bucket.ui || bucket.apps));
|
|
48
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The preview payload's now-line. An explicit `now` passes through. With only a
|
|
54
|
+
* `timezone`, the payload carries the date that zone resolved to: the preview
|
|
55
|
+
* draws an omitted `now` at the viewer's own date and cannot read the zone.
|
|
56
|
+
*/
|
|
57
|
+
function previewNow(args, today) {
|
|
58
|
+
if (args.now !== undefined)
|
|
59
|
+
return args.now;
|
|
60
|
+
if (args.timezone !== undefined && today)
|
|
61
|
+
return today.toISOString().slice(0, 10);
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
49
64
|
function leanPreviewBlock(payload) {
|
|
50
65
|
return {
|
|
51
66
|
type: 'text',
|
|
@@ -55,6 +70,7 @@ function leanPreviewBlock(payload) {
|
|
|
55
70
|
theme: payload.theme,
|
|
56
71
|
now: payload.now,
|
|
57
72
|
width: payload.width,
|
|
73
|
+
nonWorking: payload.nonWorking,
|
|
58
74
|
locale: payload.locale,
|
|
59
75
|
}),
|
|
60
76
|
};
|
|
@@ -77,11 +93,10 @@ function mutatingTool(title, opts) {
|
|
|
77
93
|
};
|
|
78
94
|
}
|
|
79
95
|
// ---- Server factory ---------------------------------------------------------
|
|
80
|
-
function resolveAndGuard(filePath,
|
|
81
|
-
const abs = path.resolve(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
throw new PathOutsideRootError(filePath, allowedRoot);
|
|
96
|
+
function resolveAndGuard(filePath, roots) {
|
|
97
|
+
const abs = path.resolve(roots.primary, filePath);
|
|
98
|
+
if (!isInsideRoots(abs, roots)) {
|
|
99
|
+
throw new PathOutsideRootError(filePath, roots.allowed.join(', '));
|
|
85
100
|
}
|
|
86
101
|
return abs;
|
|
87
102
|
}
|
|
@@ -112,10 +127,6 @@ function createNodeHostEnv(sourcePath) {
|
|
|
112
127
|
};
|
|
113
128
|
}
|
|
114
129
|
// ---- Helpers ----------------------------------------------------------------
|
|
115
|
-
function todayUtc() {
|
|
116
|
-
const now = new Date();
|
|
117
|
-
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
118
|
-
}
|
|
119
130
|
function exampleShortName(fullName) {
|
|
120
131
|
return fullName.endsWith('.nowline') ? fullName.slice(0, -'.nowline'.length) : fullName;
|
|
121
132
|
}
|
|
@@ -123,19 +134,18 @@ function findExample(name) {
|
|
|
123
134
|
const withExt = name.endsWith('.nowline') ? name : `${name}.nowline`;
|
|
124
135
|
return EXAMPLES.find((e) => e.name === withExt || e.name === name);
|
|
125
136
|
}
|
|
126
|
-
async function sourceAndPath(args,
|
|
137
|
+
async function sourceAndPath(args, roots) {
|
|
127
138
|
if (args.source !== undefined && args.path === undefined) {
|
|
128
|
-
return { source: args.source, filePath: path.join(
|
|
139
|
+
return { source: args.source, filePath: path.join(roots.primary, 'unnamed.nowline') };
|
|
129
140
|
}
|
|
130
141
|
if (args.path !== undefined) {
|
|
131
|
-
const abs = resolveAndGuard(args.path,
|
|
142
|
+
const abs = resolveAndGuard(args.path, roots);
|
|
132
143
|
const source = args.source ?? (await fs.readFile(abs, 'utf-8'));
|
|
133
144
|
return { source, filePath: abs };
|
|
134
145
|
}
|
|
135
146
|
throw new InputRequiredError('At least one of `source` or `path` is required.');
|
|
136
147
|
}
|
|
137
148
|
export function createMcpServer(opts = {}) {
|
|
138
|
-
const allowedRoot = opts.allowedRoot ?? process.cwd();
|
|
139
149
|
const rootConfigured = opts.rootConfigured ?? false;
|
|
140
150
|
const server = new McpServer({
|
|
141
151
|
name: opts.name ?? 'nowline',
|
|
@@ -158,6 +168,7 @@ export function createMcpServer(opts = {}) {
|
|
|
158
168
|
'Use the `share` tool only when the user explicitly wants a link they can open or share ' +
|
|
159
169
|
'(opens the free Nowline web app, where they can also export).',
|
|
160
170
|
});
|
|
171
|
+
const workspaceRoots = createWorkspaceRoots(server, opts.allowedRoot);
|
|
161
172
|
// ---- Resources ----------------------------------------------------------
|
|
162
173
|
server.registerResource('nowline-reference', 'nowline://reference', {
|
|
163
174
|
title: 'Roadmap Reference',
|
|
@@ -206,6 +217,31 @@ export function createMcpServer(opts = {}) {
|
|
|
206
217
|
}));
|
|
207
218
|
// ---- Prompts ------------------------------------------------------------
|
|
208
219
|
registerPrompts(server);
|
|
220
|
+
const nonWorkingSchema = z
|
|
221
|
+
.enum(NON_WORKING_DISPLAYS)
|
|
222
|
+
.optional()
|
|
223
|
+
.describe("How a business calendar's non-working days appear: hide collapses them, " +
|
|
224
|
+
"show draws them as shaded bands. Overrides the file's `default roadmap non-working:` key; " +
|
|
225
|
+
'omit to let the key apply (then hide). The schedule exports (mermaid, msproj, xlsx) are the same either way.');
|
|
226
|
+
// A string, not an enum, so `Greyscale` and the `greyscale` alias reach
|
|
227
|
+
// `resolveMcpOptions`, which rejects an unknown name with the theme list.
|
|
228
|
+
const themeSchema = z
|
|
229
|
+
.string()
|
|
230
|
+
.optional()
|
|
231
|
+
.describe(`Color theme: ${THEME_NAMES.join(', ')} ("greyscale" is accepted). Defaults to light.`);
|
|
232
|
+
const nowSchema = z
|
|
233
|
+
.string()
|
|
234
|
+
.regex(/^(\d{4}-\d{2}-\d{2}|-)$/, 'Expected YYYY-MM-DD or "-".')
|
|
235
|
+
.optional()
|
|
236
|
+
.describe("Now-line date as YYYY-MM-DD. Omit to draw it at today's date in `timezone` " +
|
|
237
|
+
"(the server's local zone when `timezone` is omitted too); " +
|
|
238
|
+
'pass "-" to suppress the now-line (same as the CLI\'s `--now -`).');
|
|
239
|
+
const timezoneSchema = z
|
|
240
|
+
.string()
|
|
241
|
+
.optional()
|
|
242
|
+
.describe("Timezone that decides today's date when `now` is omitted (ignored when `now` is a date): " +
|
|
243
|
+
'"local" (the default), "UTC", an ISO 8601 offset ("+05:30", "-07:00", "Z"), ' +
|
|
244
|
+
'or an IANA name ("America/Los_Angeles"). The same forms as the CLI\'s `--timezone`.');
|
|
209
245
|
// ---- validate -----------------------------------------------------------
|
|
210
246
|
server.registerTool('validate', {
|
|
211
247
|
description: toolDescriptionWithSyntax('Parse and validate a .nowline roadmap. Returns ok=true with optional layout insights when valid, or ok=false with structured diagnostics.'),
|
|
@@ -215,25 +251,34 @@ export function createMcpServer(opts = {}) {
|
|
|
215
251
|
.string()
|
|
216
252
|
.optional()
|
|
217
253
|
.describe('Absolute or relative path to a .nowline file to validate.'),
|
|
254
|
+
now: nowSchema,
|
|
255
|
+
timezone: timezoneSchema,
|
|
218
256
|
}),
|
|
219
257
|
outputSchema: ValidateOutputSchema,
|
|
220
258
|
annotations: readOnlyTool('Validate Roadmap'),
|
|
221
259
|
}, async (args) => {
|
|
222
260
|
try {
|
|
223
|
-
const {
|
|
261
|
+
const { options } = resolveMcpOptions(args);
|
|
262
|
+
const { source, filePath } = await sourceAndPath(args, await workspaceRoots());
|
|
224
263
|
const doc = await buildDocument(source);
|
|
225
264
|
const diagnostics = collectMcpDiagnostics(doc, filePath);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
265
|
+
// One stage: the routed (wave-rule) include diagnostics join
|
|
266
|
+
// the validator's, with the file and line they point into
|
|
267
|
+
// (specs/waves.md §6.1), and the insights come from the same
|
|
268
|
+
// layout, logo included.
|
|
269
|
+
let insights = [];
|
|
270
|
+
if (diagnostics.every((d) => d.severity !== 'error')) {
|
|
271
|
+
const stage = await stageMcpRoadmap(doc, source, {
|
|
272
|
+
sourcePath: filePath,
|
|
273
|
+
today: options.today,
|
|
232
274
|
locale: 'en-US',
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
275
|
+
theme: 'light',
|
|
276
|
+
}, createNodeHostEnv(filePath));
|
|
277
|
+
diagnostics.push(...stageRoutedDiagnostics(stage));
|
|
278
|
+
if (stage.kind === 'staged')
|
|
279
|
+
insights = layoutInsightsFromStage(stage);
|
|
280
|
+
}
|
|
281
|
+
const ok = diagnostics.every((d) => d.severity !== 'error');
|
|
237
282
|
const structured = {
|
|
238
283
|
ok,
|
|
239
284
|
diagnostics,
|
|
@@ -265,7 +310,7 @@ export function createMcpServer(opts = {}) {
|
|
|
265
310
|
annotations: readOnlyTool('Read Roadmap'),
|
|
266
311
|
}, async (args) => {
|
|
267
312
|
try {
|
|
268
|
-
const abs = resolveAndGuard(args.path,
|
|
313
|
+
const abs = resolveAndGuard(args.path, await workspaceRoots());
|
|
269
314
|
const source = await fs.readFile(abs, 'utf-8');
|
|
270
315
|
const structured = { path: abs, source };
|
|
271
316
|
return {
|
|
@@ -292,7 +337,7 @@ export function createMcpServer(opts = {}) {
|
|
|
292
337
|
}),
|
|
293
338
|
}, async (args) => {
|
|
294
339
|
try {
|
|
295
|
-
const abs = resolveAndGuard(args.path,
|
|
340
|
+
const abs = resolveAndGuard(args.path, await workspaceRoots());
|
|
296
341
|
const blocked = await diagnosticsErrorBlock(args.source, abs);
|
|
297
342
|
if (!blocked.ok)
|
|
298
343
|
return blocked.response;
|
|
@@ -324,7 +369,7 @@ export function createMcpServer(opts = {}) {
|
|
|
324
369
|
}),
|
|
325
370
|
}, async (args) => {
|
|
326
371
|
try {
|
|
327
|
-
const abs = resolveAndGuard(args.path,
|
|
372
|
+
const abs = resolveAndGuard(args.path, await workspaceRoots());
|
|
328
373
|
const blocked = await diagnosticsErrorBlock(args.source, abs);
|
|
329
374
|
if (!blocked.ok)
|
|
330
375
|
return blocked.response;
|
|
@@ -351,7 +396,7 @@ export function createMcpServer(opts = {}) {
|
|
|
351
396
|
annotations: mutatingTool('Delete Roadmap', { destructiveHint: true }),
|
|
352
397
|
}, async (args) => {
|
|
353
398
|
try {
|
|
354
|
-
const abs = resolveAndGuard(args.path,
|
|
399
|
+
const abs = resolveAndGuard(args.path, await workspaceRoots());
|
|
355
400
|
await fs.unlink(abs);
|
|
356
401
|
const structured = { path: abs };
|
|
357
402
|
return {
|
|
@@ -370,7 +415,7 @@ export function createMcpServer(opts = {}) {
|
|
|
370
415
|
directory: z
|
|
371
416
|
.string()
|
|
372
417
|
.optional()
|
|
373
|
-
.describe(
|
|
418
|
+
.describe("Directory to scan. Defaults to the project root (--root, else the client's first MCP root, else cwd). Absolute or relative."),
|
|
374
419
|
recursive: z
|
|
375
420
|
.boolean()
|
|
376
421
|
.optional()
|
|
@@ -380,9 +425,8 @@ export function createMcpServer(opts = {}) {
|
|
|
380
425
|
annotations: readOnlyTool('List Roadmaps'),
|
|
381
426
|
}, async (args) => {
|
|
382
427
|
try {
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
: allowedRoot;
|
|
428
|
+
const roots = await workspaceRoots();
|
|
429
|
+
const dir = args.directory ? resolveAndGuard(args.directory, roots) : roots.primary;
|
|
386
430
|
const recursive = args.recursive ?? false;
|
|
387
431
|
const paths = await listNowlineFiles(dir, recursive);
|
|
388
432
|
const structured = { paths };
|
|
@@ -412,15 +456,14 @@ export function createMcpServer(opts = {}) {
|
|
|
412
456
|
.enum(['svg', 'png'])
|
|
413
457
|
.optional()
|
|
414
458
|
.describe('Output format. Defaults to svg.'),
|
|
415
|
-
theme:
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
.string()
|
|
459
|
+
theme: themeSchema,
|
|
460
|
+
now: nowSchema,
|
|
461
|
+
timezone: timezoneSchema,
|
|
462
|
+
width: z
|
|
463
|
+
.number()
|
|
421
464
|
.optional()
|
|
422
|
-
.describe('
|
|
423
|
-
|
|
465
|
+
.describe('Maximum canvas width in px, an integer of at least 320. Unset fits the content; a wider roadmap shrinks its timeline to fit.'),
|
|
466
|
+
nonWorking: nonWorkingSchema,
|
|
424
467
|
scale: z
|
|
425
468
|
.number()
|
|
426
469
|
.optional()
|
|
@@ -451,10 +494,18 @@ export function createMcpServer(opts = {}) {
|
|
|
451
494
|
// Only the filesystem touchpoints (reading the input, writing the
|
|
452
495
|
// output) map to structured NL.MCP.* errors; a kernel render fault
|
|
453
496
|
// bubbles unchanged so it is never mislabeled as a path/IO failure.
|
|
497
|
+
let resolved;
|
|
498
|
+
try {
|
|
499
|
+
resolved = resolveMcpOptions(args);
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
return handleToolError(err);
|
|
503
|
+
}
|
|
504
|
+
const { theme, options } = resolved;
|
|
454
505
|
let source;
|
|
455
506
|
let filePath;
|
|
456
507
|
try {
|
|
457
|
-
({ source, filePath } = await sourceAndPath(args,
|
|
508
|
+
({ source, filePath } = await sourceAndPath(args, await workspaceRoots()));
|
|
458
509
|
}
|
|
459
510
|
catch (err) {
|
|
460
511
|
return handleToolError(err, args.path);
|
|
@@ -463,13 +514,14 @@ export function createMcpServer(opts = {}) {
|
|
|
463
514
|
if (!blocked.ok)
|
|
464
515
|
return blocked.response;
|
|
465
516
|
const format = args.format ?? 'svg';
|
|
466
|
-
const today =
|
|
517
|
+
const today = options.today;
|
|
467
518
|
const inputs = {
|
|
468
519
|
sourcePath: filePath,
|
|
469
520
|
today,
|
|
470
521
|
locale: 'en-US',
|
|
471
|
-
theme:
|
|
472
|
-
width:
|
|
522
|
+
theme: options.theme,
|
|
523
|
+
width: options.width,
|
|
524
|
+
nonWorking: options.nonWorking,
|
|
473
525
|
pngScale: args.scale,
|
|
474
526
|
};
|
|
475
527
|
const host = createNodeHostEnv(filePath);
|
|
@@ -482,34 +534,42 @@ export function createMcpServer(opts = {}) {
|
|
|
482
534
|
const result = await resolveFonts({ headless: true });
|
|
483
535
|
inputs.fonts = { sans: result.sans, mono: result.mono };
|
|
484
536
|
}
|
|
485
|
-
|
|
486
|
-
|
|
537
|
+
// One stage serves the render, the insights and the include check.
|
|
538
|
+
const stage = await stageMcpRoadmap(blocked.doc, source, inputs, host);
|
|
539
|
+
if (stage.kind === 'invalid') {
|
|
540
|
+
return diagnosticsErrorResponse(filePath, collectMcpDiagnostics(blocked.doc, filePath));
|
|
541
|
+
}
|
|
542
|
+
// Wave-rule include errors are reported as diagnostics. An uncoded
|
|
543
|
+
// include error throws from a render, as the kernel's does; the
|
|
544
|
+
// preview path, which draws nothing here, keeps the preview it
|
|
545
|
+
// returned before waves existed (specs/waves.md §6.1).
|
|
546
|
+
const routed = stageRoutedDiagnostics(stage);
|
|
547
|
+
if (routed.some((d) => d.severity === 'error')) {
|
|
548
|
+
return includeDiagnosticsResponse(blocked.doc, filePath, routed);
|
|
549
|
+
}
|
|
550
|
+
if (stage.kind === 'include-error' && needsRender)
|
|
551
|
+
acceptStagedRoadmap(stage, inputs);
|
|
552
|
+
const staged = stage.kind === 'staged' ? stage : undefined;
|
|
553
|
+
const insights = staged ? layoutInsightsFromStage(staged) : [];
|
|
554
|
+
const bytes = needsRender && staged
|
|
555
|
+
? await exportStaged(staged, format, inputs, host)
|
|
487
556
|
: new Uint8Array(0);
|
|
488
|
-
const insights = await collectMcpLayoutInsights({
|
|
489
|
-
source,
|
|
490
|
-
filePath,
|
|
491
|
-
today,
|
|
492
|
-
theme: args.theme ?? 'light',
|
|
493
|
-
width: args.width,
|
|
494
|
-
locale: 'en-US',
|
|
495
|
-
readFile: host.readSource,
|
|
496
|
-
doc: blocked.doc,
|
|
497
|
-
});
|
|
498
557
|
const previewPayload = {
|
|
499
558
|
source,
|
|
500
|
-
theme
|
|
501
|
-
now: args.
|
|
502
|
-
width:
|
|
559
|
+
theme,
|
|
560
|
+
now: previewNow(args, options.today),
|
|
561
|
+
width: options.width,
|
|
562
|
+
nonWorking: options.nonWorking,
|
|
503
563
|
locale: 'en-US',
|
|
504
564
|
};
|
|
505
565
|
const reviewBlocks = args.review === true
|
|
506
|
-
? await buildReviewContentBlocks(source, format, bytes, inputs, host)
|
|
566
|
+
? await buildReviewContentBlocks(blocked.doc, source, format, bytes, inputs, host)
|
|
507
567
|
: [];
|
|
508
568
|
const insightHintBlocks = insights.length > 0 ? [{ type: 'text', text: LAYOUT_INSIGHT_HINT }] : [];
|
|
509
569
|
const insightsField = insights.length > 0 ? { insights } : {};
|
|
510
570
|
if (args.output) {
|
|
511
571
|
try {
|
|
512
|
-
const outAbs = resolveAndGuard(args.output,
|
|
572
|
+
const outAbs = resolveAndGuard(args.output, await workspaceRoots());
|
|
513
573
|
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
514
574
|
await fs.writeFile(outAbs, bytes);
|
|
515
575
|
const structured = {
|
|
@@ -602,10 +662,15 @@ export function createMcpServer(opts = {}) {
|
|
|
602
662
|
'To choose a filename, pass a bare filename (e.g. "roadmap.pdf") — it is saved into the configured folder. ' +
|
|
603
663
|
'Never pass a virtual or sandbox path such as /home/claude/…, /mnt/user-data/…, or any path ' +
|
|
604
664
|
'outside the configured output folder — those do not exist on the host filesystem.'),
|
|
605
|
-
now:
|
|
606
|
-
|
|
665
|
+
now: nowSchema,
|
|
666
|
+
timezone: timezoneSchema,
|
|
667
|
+
theme: themeSchema,
|
|
668
|
+
nonWorking: nonWorkingSchema,
|
|
607
669
|
scale: z.number().optional().describe('PNG scale factor.'),
|
|
608
|
-
pageSize: z
|
|
670
|
+
pageSize: z
|
|
671
|
+
.string()
|
|
672
|
+
.optional()
|
|
673
|
+
.describe('PDF page size: a preset name, "content", or <W>x<H><unit> (e.g. 8.5x11in). Validated for every format.'),
|
|
609
674
|
orientation: z
|
|
610
675
|
.enum(['portrait', 'landscape', 'auto'])
|
|
611
676
|
.optional()
|
|
@@ -631,10 +696,17 @@ export function createMcpServer(opts = {}) {
|
|
|
631
696
|
// Only the filesystem touchpoints (reading the input, writing the
|
|
632
697
|
// output) map to structured NL.MCP.* errors; a kernel export fault
|
|
633
698
|
// bubbles unchanged so it is never mislabeled as a path/IO failure.
|
|
699
|
+
let options;
|
|
700
|
+
try {
|
|
701
|
+
({ options } = resolveMcpOptions(args));
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
return handleToolError(err);
|
|
705
|
+
}
|
|
634
706
|
let source;
|
|
635
707
|
let filePath;
|
|
636
708
|
try {
|
|
637
|
-
({ source, filePath } = await sourceAndPath(args,
|
|
709
|
+
({ source, filePath } = await sourceAndPath(args, await workspaceRoots()));
|
|
638
710
|
}
|
|
639
711
|
catch (err) {
|
|
640
712
|
return handleToolError(err, args.path);
|
|
@@ -643,14 +715,14 @@ export function createMcpServer(opts = {}) {
|
|
|
643
715
|
if (!blocked.ok)
|
|
644
716
|
return blocked.response;
|
|
645
717
|
const format = args.format;
|
|
646
|
-
const today = args.now ? new Date(`${args.now}T00:00:00Z`) : todayUtc();
|
|
647
718
|
const inputs = {
|
|
648
719
|
sourcePath: filePath,
|
|
649
|
-
today,
|
|
720
|
+
today: options.today,
|
|
650
721
|
locale: 'en-US',
|
|
651
|
-
theme:
|
|
722
|
+
theme: options.theme,
|
|
723
|
+
nonWorking: options.nonWorking,
|
|
652
724
|
pngScale: args.scale,
|
|
653
|
-
pageSize:
|
|
725
|
+
pageSize: options.pageSize,
|
|
654
726
|
orientation: args.orientation,
|
|
655
727
|
marginPt: args.marginPt,
|
|
656
728
|
msprojStart: args.msprojStart,
|
|
@@ -660,7 +732,16 @@ export function createMcpServer(opts = {}) {
|
|
|
660
732
|
inputs.fonts = { sans: result.sans, mono: result.mono };
|
|
661
733
|
}
|
|
662
734
|
const host = createNodeHostEnv(filePath);
|
|
663
|
-
|
|
735
|
+
let bytes;
|
|
736
|
+
try {
|
|
737
|
+
const staged = acceptStagedRoadmap(await stageMcpRoadmap(blocked.doc, source, inputs, host), inputs);
|
|
738
|
+
bytes = await exportStaged(staged, format, inputs, host);
|
|
739
|
+
}
|
|
740
|
+
catch (err) {
|
|
741
|
+
if (!(err instanceof IncludeResolveError))
|
|
742
|
+
throw err;
|
|
743
|
+
return includeDiagnosticsResponse(blocked.doc, filePath, routedResolveDiagnosticsToMcp(err.diagnostics));
|
|
744
|
+
}
|
|
664
745
|
const BINARY_FORMATS = new Set(['png', 'pdf', 'xlsx']);
|
|
665
746
|
const isBinary = BINARY_FORMATS.has(format);
|
|
666
747
|
// Default filename: <roadmap-id>.<ext>, used when no output path is given.
|
|
@@ -713,11 +794,12 @@ export function createMcpServer(opts = {}) {
|
|
|
713
794
|
}
|
|
714
795
|
}
|
|
715
796
|
// Write to disk if needed.
|
|
716
|
-
const
|
|
797
|
+
const roots = await workspaceRoots();
|
|
798
|
+
const outTarget = args.output ?? path.join(roots.primary, defaultFilename);
|
|
717
799
|
let writtenPath;
|
|
718
800
|
if (doWrite) {
|
|
719
801
|
try {
|
|
720
|
-
const outAbs = resolveAndGuard(outTarget,
|
|
802
|
+
const outAbs = resolveAndGuard(outTarget, roots);
|
|
721
803
|
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
722
804
|
await fs.writeFile(outAbs, bytes);
|
|
723
805
|
writtenPath = outAbs;
|
|
@@ -785,7 +867,7 @@ export function createMcpServer(opts = {}) {
|
|
|
785
867
|
let source;
|
|
786
868
|
let filePath;
|
|
787
869
|
try {
|
|
788
|
-
({ source, filePath } = await sourceAndPath(args,
|
|
870
|
+
({ source, filePath } = await sourceAndPath(args, await workspaceRoots()));
|
|
789
871
|
}
|
|
790
872
|
catch (err) {
|
|
791
873
|
return handleToolError(err, args.path);
|
|
@@ -822,11 +904,14 @@ export function createMcpServer(opts = {}) {
|
|
|
822
904
|
}, async (args) => {
|
|
823
905
|
try {
|
|
824
906
|
if (args.to === 'json') {
|
|
825
|
-
const { source, filePath } = await sourceAndPath(args,
|
|
907
|
+
const { source, filePath } = await sourceAndPath(args, await workspaceRoots());
|
|
826
908
|
const host = createNodeHostEnv(filePath);
|
|
909
|
+
// The JSON AST does not depend on today's date, so convert
|
|
910
|
+
// takes no `now`; the kernel still wants a concrete one.
|
|
911
|
+
const { options } = resolveMcpOptions({});
|
|
827
912
|
const jsonBytes = await exportDocument(source, 'json', {
|
|
828
913
|
sourcePath: filePath,
|
|
829
|
-
today:
|
|
914
|
+
today: options.today,
|
|
830
915
|
locale: 'en-US',
|
|
831
916
|
theme: 'light',
|
|
832
917
|
}, host);
|
|
@@ -840,7 +925,7 @@ export function createMcpServer(opts = {}) {
|
|
|
840
925
|
// to: 'nowline' — input is a JSON AST string
|
|
841
926
|
const jsonSource = args.source ??
|
|
842
927
|
(args.path
|
|
843
|
-
? await fs.readFile(resolveAndGuard(args.path,
|
|
928
|
+
? await fs.readFile(resolveAndGuard(args.path, await workspaceRoots()), 'utf-8')
|
|
844
929
|
: null);
|
|
845
930
|
if (!jsonSource) {
|
|
846
931
|
throw new InputRequiredError('At least one of `source` or `path` is required.');
|
|
@@ -878,7 +963,7 @@ export function createMcpServer(opts = {}) {
|
|
|
878
963
|
});
|
|
879
964
|
// ---- list-themes --------------------------------------------------------
|
|
880
965
|
server.registerTool('list-themes', {
|
|
881
|
-
description:
|
|
966
|
+
description: `List supported color themes: ${THEME_NAMES.join(', ')}.`,
|
|
882
967
|
inputSchema: z.object({}),
|
|
883
968
|
outputSchema: ListItemsOutputSchema,
|
|
884
969
|
annotations: readOnlyTool('List Themes'),
|
|
@@ -1012,7 +1097,7 @@ export function createMcpServer(opts = {}) {
|
|
|
1012
1097
|
};
|
|
1013
1098
|
});
|
|
1014
1099
|
server.registerTool('schema', {
|
|
1015
|
-
description: 'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties).',
|
|
1100
|
+
description: 'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties, config keywords, calendar declaration keys).',
|
|
1016
1101
|
inputSchema: z.object({}),
|
|
1017
1102
|
outputSchema: SchemaOutputSchema,
|
|
1018
1103
|
annotations: readOnlyTool('View Roadmap Schema'),
|
|
@@ -1021,6 +1106,8 @@ export function createMcpServer(opts = {}) {
|
|
|
1021
1106
|
directiveKeys: [...SCHEMA_VOCABULARY.directiveKeys],
|
|
1022
1107
|
entityTypes: [...SCHEMA_VOCABULARY.entityTypes],
|
|
1023
1108
|
itemPropertyKeys: [...SCHEMA_VOCABULARY.itemPropertyKeys],
|
|
1109
|
+
configKeywords: [...SCHEMA_VOCABULARY.configKeywords],
|
|
1110
|
+
calendarDeclarationKeys: [...SCHEMA_VOCABULARY.calendarDeclarationKeys],
|
|
1024
1111
|
};
|
|
1025
1112
|
return {
|
|
1026
1113
|
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
@@ -1068,7 +1155,7 @@ function buildExportInlineContentBlocks(format, bytes, showHint = true, rootConf
|
|
|
1068
1155
|
}
|
|
1069
1156
|
return blocks;
|
|
1070
1157
|
}
|
|
1071
|
-
async function buildReviewContentBlocks(source, format, artifactBytes, inputs, host) {
|
|
1158
|
+
async function buildReviewContentBlocks(doc, source, format, artifactBytes, inputs, host) {
|
|
1072
1159
|
const blocks = [
|
|
1073
1160
|
{
|
|
1074
1161
|
type: 'text',
|
|
@@ -1091,7 +1178,10 @@ async function buildReviewContentBlocks(source, format, artifactBytes, inputs, h
|
|
|
1091
1178
|
const fonts = await resolveFonts({ headless: true });
|
|
1092
1179
|
reviewInputs.fonts = { sans: fonts.sans, mono: fonts.mono };
|
|
1093
1180
|
}
|
|
1094
|
-
|
|
1181
|
+
// A narrower review width is a different layout, so it stages again
|
|
1182
|
+
// (the parsed document is reused).
|
|
1183
|
+
const reviewStage = acceptStagedRoadmap(await stageMcpRoadmap(doc, source, reviewInputs, host), reviewInputs);
|
|
1184
|
+
inspectionBytes = await exportStaged(reviewStage, 'png', reviewInputs, host);
|
|
1095
1185
|
}
|
|
1096
1186
|
if (format !== 'png' || inspectionBytes.byteLength !== artifactBytes.byteLength) {
|
|
1097
1187
|
blocks.push({
|