@nowline/mcp 0.8.0 → 0.8.1
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/diagnostics.d.ts +26 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +55 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/generated/ui-bundle.js +2 -2
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +3 -0
- package/dist/prompts.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +245 -142
- package/dist/server.js.map +1 -1
- package/package.json +9 -9
- package/src/diagnostics.ts +63 -0
- package/src/generated/ui-bundle.ts +2 -2
- package/src/prompts.ts +3 -0
- package/src/server.ts +248 -148
package/dist/server.js
CHANGED
|
@@ -16,7 +16,7 @@ import { buildShareLink } from '@nowline/share-link';
|
|
|
16
16
|
import { z } from 'zod';
|
|
17
17
|
import { NOWLINE_MCP_ICONS } from './branding.js';
|
|
18
18
|
import { CAPABILITIES } from './capabilities.js';
|
|
19
|
-
import { buildDocument, collectMcpDiagnostics, collectMcpLayoutInsights, DEFAULT_RENDER_WIDTH, diagnosticsErrorBlock, LAYOUT_INSIGHT_HINT, REVIEW_MAX_WIDTH, toolDescriptionWithSyntax, } from './diagnostics.js';
|
|
19
|
+
import { buildDocument, collectMcpDiagnostics, collectMcpLayoutInsights, DEFAULT_RENDER_WIDTH, diagnosticsErrorBlock, handleToolError, InputRequiredError, LAYOUT_INSIGHT_HINT, PathOutsideRootError, REVIEW_MAX_WIDTH, toolDescriptionWithSyntax, } from './diagnostics.js';
|
|
20
20
|
import { CONVERSIONS_GUIDE, EXAMPLES, REFERENCE_MAN_PAGE, } from './generated/resources.js';
|
|
21
21
|
import { PREVIEW_HTML } from './generated/ui-bundle.js';
|
|
22
22
|
import { registerPrompts } from './prompts.js';
|
|
@@ -59,12 +59,29 @@ function leanPreviewBlock(payload) {
|
|
|
59
59
|
}),
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
// ---- Tool annotation presets (Anthropic Software Directory Policy § 5.E) ---
|
|
63
|
+
function readOnlyTool(title) {
|
|
64
|
+
return {
|
|
65
|
+
title,
|
|
66
|
+
readOnlyHint: true,
|
|
67
|
+
idempotentHint: true,
|
|
68
|
+
openWorldHint: false,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function mutatingTool(title, opts) {
|
|
72
|
+
return {
|
|
73
|
+
title,
|
|
74
|
+
destructiveHint: opts.destructiveHint,
|
|
75
|
+
...(opts.idempotentHint ? { idempotentHint: true } : {}),
|
|
76
|
+
openWorldHint: false,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
62
79
|
// ---- Server factory ---------------------------------------------------------
|
|
63
80
|
function resolveAndGuard(filePath, allowedRoot) {
|
|
64
81
|
const abs = path.resolve(allowedRoot, filePath);
|
|
65
82
|
const guard = path.resolve(allowedRoot);
|
|
66
83
|
if (!abs.startsWith(guard + path.sep) && abs !== guard) {
|
|
67
|
-
throw new
|
|
84
|
+
throw new PathOutsideRootError(filePath, allowedRoot);
|
|
68
85
|
}
|
|
69
86
|
return abs;
|
|
70
87
|
}
|
|
@@ -115,7 +132,7 @@ async function sourceAndPath(args, allowedRoot) {
|
|
|
115
132
|
const source = args.source ?? (await fs.readFile(abs, 'utf-8'));
|
|
116
133
|
return { source, filePath: abs };
|
|
117
134
|
}
|
|
118
|
-
throw new
|
|
135
|
+
throw new InputRequiredError('At least one of `source` or `path` is required.');
|
|
119
136
|
}
|
|
120
137
|
export function createMcpServer(opts = {}) {
|
|
121
138
|
const allowedRoot = opts.allowedRoot ?? process.cwd();
|
|
@@ -133,6 +150,7 @@ export function createMcpServer(opts = {}) {
|
|
|
133
150
|
});
|
|
134
151
|
// ---- Resources ----------------------------------------------------------
|
|
135
152
|
server.registerResource('nowline-reference', 'nowline://reference', {
|
|
153
|
+
title: 'Roadmap Reference',
|
|
136
154
|
description: 'Full DSL reference (nowline.5 man page): syntax, directives, and examples.',
|
|
137
155
|
mimeType: 'text/plain',
|
|
138
156
|
}, async () => ({
|
|
@@ -141,6 +159,7 @@ export function createMcpServer(opts = {}) {
|
|
|
141
159
|
],
|
|
142
160
|
}));
|
|
143
161
|
server.registerResource('nowline-examples', 'nowline://examples', {
|
|
162
|
+
title: 'Roadmap Examples',
|
|
144
163
|
description: 'Canonical example .nowline files from the official examples/ directory.',
|
|
145
164
|
mimeType: 'text/plain',
|
|
146
165
|
}, async () => ({
|
|
@@ -151,6 +170,7 @@ export function createMcpServer(opts = {}) {
|
|
|
151
170
|
})),
|
|
152
171
|
}));
|
|
153
172
|
server.registerResource('nowline-conversions', 'nowline://conversions', {
|
|
173
|
+
title: 'Conversion Guide',
|
|
154
174
|
description: 'LLM-mediated conversion guide: how to translate Mermaid gantt, MS Project, Excel, Google Sheets timeline, and generic CSV into Nowline DSL.',
|
|
155
175
|
mimeType: 'text/plain',
|
|
156
176
|
}, async () => ({
|
|
@@ -163,6 +183,7 @@ export function createMcpServer(opts = {}) {
|
|
|
163
183
|
],
|
|
164
184
|
}));
|
|
165
185
|
registerAppResource(server, 'nowline-preview', PREVIEW_UI_URI, {
|
|
186
|
+
title: 'Roadmap Preview',
|
|
166
187
|
description: 'Interactive in-chat roadmap preview (MCP Apps). Hydrates via ontoolresult.',
|
|
167
188
|
}, async () => ({
|
|
168
189
|
contents: [
|
|
@@ -186,32 +207,41 @@ export function createMcpServer(opts = {}) {
|
|
|
186
207
|
.describe('Absolute or relative path to a .nowline file to validate.'),
|
|
187
208
|
}),
|
|
188
209
|
outputSchema: ValidateOutputSchema,
|
|
189
|
-
annotations:
|
|
210
|
+
annotations: readOnlyTool('Validate Roadmap'),
|
|
190
211
|
}, async (args) => {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
...(insights.length > 0
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
try {
|
|
213
|
+
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
214
|
+
const doc = await buildDocument(source);
|
|
215
|
+
const diagnostics = collectMcpDiagnostics(doc, filePath);
|
|
216
|
+
const ok = diagnostics.every((d) => d.severity !== 'error');
|
|
217
|
+
const insights = ok
|
|
218
|
+
? await collectMcpLayoutInsights({
|
|
219
|
+
source,
|
|
220
|
+
filePath,
|
|
221
|
+
today: todayUtc(),
|
|
222
|
+
locale: 'en-US',
|
|
223
|
+
readFile: createNodeHostEnv(filePath).readSource,
|
|
224
|
+
doc,
|
|
225
|
+
})
|
|
226
|
+
: [];
|
|
227
|
+
const structured = {
|
|
228
|
+
ok,
|
|
229
|
+
diagnostics,
|
|
230
|
+
...(insights.length > 0 ? { insights } : {}),
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
content: [
|
|
234
|
+
{ type: 'text', text: JSON.stringify(structured, null, 2) },
|
|
235
|
+
...(insights.length > 0
|
|
236
|
+
? [{ type: 'text', text: LAYOUT_INSIGHT_HINT }]
|
|
237
|
+
: []),
|
|
238
|
+
],
|
|
239
|
+
structuredContent: structured,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
return handleToolError(err, args.path);
|
|
244
|
+
}
|
|
215
245
|
});
|
|
216
246
|
// ---- read ---------------------------------------------------------------
|
|
217
247
|
server.registerTool('read', {
|
|
@@ -222,15 +252,20 @@ export function createMcpServer(opts = {}) {
|
|
|
222
252
|
.describe('Absolute or relative path to the .nowline file to read.'),
|
|
223
253
|
}),
|
|
224
254
|
outputSchema: ReadOutputSchema,
|
|
225
|
-
annotations:
|
|
255
|
+
annotations: readOnlyTool('Read Roadmap'),
|
|
226
256
|
}, async (args) => {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
257
|
+
try {
|
|
258
|
+
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
259
|
+
const source = await fs.readFile(abs, 'utf-8');
|
|
260
|
+
const structured = { path: abs, source };
|
|
261
|
+
return {
|
|
262
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
263
|
+
structuredContent: structured,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
return handleToolError(err, args.path);
|
|
268
|
+
}
|
|
234
269
|
});
|
|
235
270
|
// ---- create -------------------------------------------------------------
|
|
236
271
|
server.registerTool('create', {
|
|
@@ -241,19 +276,27 @@ export function createMcpServer(opts = {}) {
|
|
|
241
276
|
}),
|
|
242
277
|
outputSchema: CreateOutputSchema,
|
|
243
278
|
// Overwrites silently → destructive; same source always produces same file → idempotent.
|
|
244
|
-
annotations:
|
|
279
|
+
annotations: mutatingTool('Create Roadmap', {
|
|
280
|
+
destructiveHint: true,
|
|
281
|
+
idempotentHint: true,
|
|
282
|
+
}),
|
|
245
283
|
}, async (args) => {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
284
|
+
try {
|
|
285
|
+
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
286
|
+
const blocked = await diagnosticsErrorBlock(args.source, abs);
|
|
287
|
+
if (!blocked.ok)
|
|
288
|
+
return blocked.response;
|
|
289
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
290
|
+
await fs.writeFile(abs, args.source, 'utf-8');
|
|
291
|
+
const structured = { ok: true, path: abs };
|
|
292
|
+
return {
|
|
293
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
294
|
+
structuredContent: structured,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
return handleToolError(err, args.path);
|
|
299
|
+
}
|
|
257
300
|
});
|
|
258
301
|
// ---- update -------------------------------------------------------------
|
|
259
302
|
server.registerTool('update', {
|
|
@@ -265,18 +308,26 @@ export function createMcpServer(opts = {}) {
|
|
|
265
308
|
source: z.string().describe('The new .nowline source text.'),
|
|
266
309
|
}),
|
|
267
310
|
outputSchema: UpdateOutputSchema,
|
|
268
|
-
annotations:
|
|
311
|
+
annotations: mutatingTool('Update Roadmap', {
|
|
312
|
+
destructiveHint: true,
|
|
313
|
+
idempotentHint: true,
|
|
314
|
+
}),
|
|
269
315
|
}, async (args) => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
316
|
+
try {
|
|
317
|
+
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
318
|
+
const blocked = await diagnosticsErrorBlock(args.source, abs);
|
|
319
|
+
if (!blocked.ok)
|
|
320
|
+
return blocked.response;
|
|
321
|
+
await fs.writeFile(abs, args.source, 'utf-8');
|
|
322
|
+
const structured = { ok: true, path: abs };
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
325
|
+
structuredContent: structured,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
catch (err) {
|
|
329
|
+
return handleToolError(err, args.path);
|
|
330
|
+
}
|
|
280
331
|
});
|
|
281
332
|
// ---- delete -------------------------------------------------------------
|
|
282
333
|
server.registerTool('delete', {
|
|
@@ -287,15 +338,20 @@ export function createMcpServer(opts = {}) {
|
|
|
287
338
|
.describe('Absolute or relative path of the .nowline file to delete.'),
|
|
288
339
|
}),
|
|
289
340
|
outputSchema: DeleteOutputSchema,
|
|
290
|
-
annotations: { destructiveHint: true },
|
|
341
|
+
annotations: mutatingTool('Delete Roadmap', { destructiveHint: true }),
|
|
291
342
|
}, async (args) => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
343
|
+
try {
|
|
344
|
+
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
345
|
+
await fs.unlink(abs);
|
|
346
|
+
const structured = { path: abs };
|
|
347
|
+
return {
|
|
348
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
349
|
+
structuredContent: structured,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
return handleToolError(err, args.path);
|
|
354
|
+
}
|
|
299
355
|
});
|
|
300
356
|
// ---- list ---------------------------------------------------------------
|
|
301
357
|
server.registerTool('list', {
|
|
@@ -311,16 +367,23 @@ export function createMcpServer(opts = {}) {
|
|
|
311
367
|
.describe('Whether to scan subdirectories. Defaults to false.'),
|
|
312
368
|
}),
|
|
313
369
|
outputSchema: ListOutputSchema,
|
|
314
|
-
annotations:
|
|
370
|
+
annotations: readOnlyTool('List Roadmaps'),
|
|
315
371
|
}, async (args) => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
372
|
+
try {
|
|
373
|
+
const dir = args.directory
|
|
374
|
+
? resolveAndGuard(args.directory, allowedRoot)
|
|
375
|
+
: allowedRoot;
|
|
376
|
+
const recursive = args.recursive ?? false;
|
|
377
|
+
const paths = await listNowlineFiles(dir, recursive);
|
|
378
|
+
const structured = { paths };
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
381
|
+
structuredContent: structured,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
return handleToolError(err, args.directory);
|
|
386
|
+
}
|
|
324
387
|
});
|
|
325
388
|
// ---- render -------------------------------------------------------------
|
|
326
389
|
registerAppTool(server, 'render', {
|
|
@@ -371,13 +434,23 @@ export function createMcpServer(opts = {}) {
|
|
|
371
434
|
.describe('When true, force the in-chat MCP Apps preview. On MCP Apps hosts the preview auto-renders via _meta.ui without this flag.'),
|
|
372
435
|
}),
|
|
373
436
|
outputSchema: RenderOutputSchema,
|
|
374
|
-
annotations:
|
|
437
|
+
annotations: readOnlyTool('Render Roadmap'),
|
|
375
438
|
_meta: {
|
|
376
439
|
ui: { resourceUri: PREVIEW_UI_URI },
|
|
377
440
|
'openai/outputTemplate': PREVIEW_UI_URI,
|
|
378
441
|
},
|
|
379
442
|
}, async (args) => {
|
|
380
|
-
|
|
443
|
+
// Only the filesystem touchpoints (reading the input, writing the
|
|
444
|
+
// output) map to structured NL.MCP.* errors; a kernel render fault
|
|
445
|
+
// bubbles unchanged so it is never mislabeled as a path/IO failure.
|
|
446
|
+
let source;
|
|
447
|
+
let filePath;
|
|
448
|
+
try {
|
|
449
|
+
({ source, filePath } = await sourceAndPath(args, allowedRoot));
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
return handleToolError(err, args.path);
|
|
453
|
+
}
|
|
381
454
|
const blocked = await diagnosticsErrorBlock(source, filePath);
|
|
382
455
|
if (!blocked.ok)
|
|
383
456
|
return blocked.response;
|
|
@@ -429,43 +502,53 @@ export function createMcpServer(opts = {}) {
|
|
|
429
502
|
const insightHintBlocks = insights.length > 0 ? [{ type: 'text', text: LAYOUT_INSIGHT_HINT }] : [];
|
|
430
503
|
const insightsField = insights.length > 0 ? { insights } : {};
|
|
431
504
|
if (args.output) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
505
|
+
try {
|
|
506
|
+
const outAbs = resolveAndGuard(args.output, allowedRoot);
|
|
507
|
+
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
508
|
+
await fs.writeFile(outAbs, bytes);
|
|
509
|
+
const structured = {
|
|
510
|
+
format,
|
|
511
|
+
path: outAbs,
|
|
512
|
+
bytes: bytes.byteLength,
|
|
513
|
+
shareUrl,
|
|
514
|
+
...insightsField,
|
|
515
|
+
};
|
|
516
|
+
return {
|
|
517
|
+
content: [
|
|
518
|
+
...(appActive ? [leanPreviewBlock(previewPayload)] : []),
|
|
519
|
+
{ type: 'text', text: JSON.stringify(structured, null, 2) },
|
|
520
|
+
...insightHintBlocks,
|
|
521
|
+
...reviewBlocks,
|
|
522
|
+
],
|
|
523
|
+
structuredContent: structured,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
return handleToolError(err, args.output);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (appActive) {
|
|
435
531
|
const structured = {
|
|
436
532
|
format,
|
|
437
|
-
path: outAbs,
|
|
438
|
-
bytes: bytes.byteLength,
|
|
439
533
|
shareUrl,
|
|
440
534
|
...insightsField,
|
|
441
535
|
};
|
|
442
536
|
return {
|
|
443
537
|
content: [
|
|
444
|
-
|
|
445
|
-
{ type: 'text', text: JSON.stringify(structured, null, 2) },
|
|
538
|
+
leanPreviewBlock(previewPayload),
|
|
446
539
|
...insightHintBlocks,
|
|
447
540
|
...reviewBlocks,
|
|
448
541
|
],
|
|
449
542
|
structuredContent: structured,
|
|
450
543
|
};
|
|
451
544
|
}
|
|
452
|
-
if (
|
|
545
|
+
if (format === 'png') {
|
|
453
546
|
const structured = {
|
|
454
547
|
format,
|
|
548
|
+
bytes: bytes.byteLength,
|
|
455
549
|
shareUrl,
|
|
456
550
|
...insightsField,
|
|
457
551
|
};
|
|
458
|
-
return {
|
|
459
|
-
content: [
|
|
460
|
-
leanPreviewBlock(previewPayload),
|
|
461
|
-
...insightHintBlocks,
|
|
462
|
-
...reviewBlocks,
|
|
463
|
-
],
|
|
464
|
-
structuredContent: structured,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
if (format === 'png') {
|
|
468
|
-
const structured = { format, bytes: bytes.byteLength, shareUrl, ...insightsField };
|
|
469
552
|
return {
|
|
470
553
|
content: [
|
|
471
554
|
{
|
|
@@ -531,9 +614,19 @@ export function createMcpServer(opts = {}) {
|
|
|
531
614
|
.describe('When true, include a shareUrl pointing to https://free.nowline.io/open.'),
|
|
532
615
|
}),
|
|
533
616
|
outputSchema: ExportOutputSchema,
|
|
534
|
-
annotations:
|
|
617
|
+
annotations: readOnlyTool('Export Roadmap'),
|
|
535
618
|
}, async (args) => {
|
|
536
|
-
|
|
619
|
+
// Only the filesystem touchpoints (reading the input, writing the
|
|
620
|
+
// output) map to structured NL.MCP.* errors; a kernel export fault
|
|
621
|
+
// bubbles unchanged so it is never mislabeled as a path/IO failure.
|
|
622
|
+
let source;
|
|
623
|
+
let filePath;
|
|
624
|
+
try {
|
|
625
|
+
({ source, filePath } = await sourceAndPath(args, allowedRoot));
|
|
626
|
+
}
|
|
627
|
+
catch (err) {
|
|
628
|
+
return handleToolError(err, args.path);
|
|
629
|
+
}
|
|
537
630
|
const blocked = await diagnosticsErrorBlock(source, filePath);
|
|
538
631
|
if (!blocked.ok)
|
|
539
632
|
return blocked.response;
|
|
@@ -562,14 +655,19 @@ export function createMcpServer(opts = {}) {
|
|
|
562
655
|
const BINARY_FORMATS = new Set(['png', 'pdf', 'xlsx']);
|
|
563
656
|
const isBinary = BINARY_FORMATS.has(format);
|
|
564
657
|
if (args.output) {
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
658
|
+
try {
|
|
659
|
+
const outAbs = resolveAndGuard(args.output, allowedRoot);
|
|
660
|
+
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
661
|
+
await fs.writeFile(outAbs, bytes);
|
|
662
|
+
const structured = { format, path: outAbs, bytes: bytes.byteLength, shareUrl };
|
|
663
|
+
return {
|
|
664
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
665
|
+
structuredContent: structured,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
return handleToolError(err, args.output);
|
|
670
|
+
}
|
|
573
671
|
}
|
|
574
672
|
if (isBinary) {
|
|
575
673
|
const mimeMap = {
|
|
@@ -612,46 +710,51 @@ export function createMcpServer(opts = {}) {
|
|
|
612
710
|
.describe('"json" — serialize .nowline text to JSON AST. "nowline" — pretty-print a JSON AST back to .nowline source.'),
|
|
613
711
|
}),
|
|
614
712
|
outputSchema: ConvertOutputSchema,
|
|
615
|
-
annotations:
|
|
713
|
+
annotations: readOnlyTool('Convert Roadmap'),
|
|
616
714
|
}, async (args) => {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
715
|
+
try {
|
|
716
|
+
if (args.to === 'json') {
|
|
717
|
+
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
718
|
+
const host = createNodeHostEnv(filePath);
|
|
719
|
+
const jsonBytes = await exportDocument(source, 'json', {
|
|
720
|
+
sourcePath: filePath,
|
|
721
|
+
today: todayUtc(),
|
|
722
|
+
locale: 'en-US',
|
|
723
|
+
theme: 'light',
|
|
724
|
+
}, host);
|
|
725
|
+
const result = new TextDecoder('utf-8').decode(jsonBytes);
|
|
726
|
+
const structured = { to: 'json', result };
|
|
727
|
+
return {
|
|
728
|
+
content: [{ type: 'text', text: result }],
|
|
729
|
+
structuredContent: structured,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
// to: 'nowline' — input is a JSON AST string
|
|
733
|
+
const jsonSource = args.source ??
|
|
734
|
+
(args.path
|
|
735
|
+
? await fs.readFile(resolveAndGuard(args.path, allowedRoot), 'utf-8')
|
|
736
|
+
: null);
|
|
737
|
+
if (!jsonSource) {
|
|
738
|
+
throw new InputRequiredError('At least one of `source` or `path` is required.');
|
|
739
|
+
}
|
|
740
|
+
const { ast } = parseNowlineJson(jsonSource, args.path ?? 'input.json');
|
|
741
|
+
const result = printNowlineFile(ast);
|
|
742
|
+
const structured = { to: 'nowline', result };
|
|
628
743
|
return {
|
|
629
744
|
content: [{ type: 'text', text: result }],
|
|
630
745
|
structuredContent: structured,
|
|
631
746
|
};
|
|
632
747
|
}
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
(args.path
|
|
636
|
-
? await fs.readFile(resolveAndGuard(args.path, allowedRoot), 'utf-8')
|
|
637
|
-
: null);
|
|
638
|
-
if (!jsonSource) {
|
|
639
|
-
throw new Error('At least one of `source` or `path` is required.');
|
|
748
|
+
catch (err) {
|
|
749
|
+
return handleToolError(err, args.path);
|
|
640
750
|
}
|
|
641
|
-
const { ast } = parseNowlineJson(jsonSource, args.path ?? 'input.json');
|
|
642
|
-
const result = printNowlineFile(ast);
|
|
643
|
-
const structured = { to: 'nowline', result };
|
|
644
|
-
return {
|
|
645
|
-
content: [{ type: 'text', text: result }],
|
|
646
|
-
structuredContent: structured,
|
|
647
|
-
};
|
|
648
751
|
});
|
|
649
752
|
// ---- capabilities -------------------------------------------------------
|
|
650
753
|
server.registerTool('capabilities', {
|
|
651
754
|
description: 'Return all supported themes, icons, locales, export formats, and template names in a single response.',
|
|
652
755
|
inputSchema: z.object({}),
|
|
653
756
|
outputSchema: CapabilitiesOutputSchema,
|
|
654
|
-
annotations:
|
|
757
|
+
annotations: readOnlyTool('View Roadmap Capabilities'),
|
|
655
758
|
}, async () => {
|
|
656
759
|
const structured = {
|
|
657
760
|
themes: [...CAPABILITIES.themes],
|
|
@@ -670,7 +773,7 @@ export function createMcpServer(opts = {}) {
|
|
|
670
773
|
description: 'List supported color themes: light, dark, grayscale.',
|
|
671
774
|
inputSchema: z.object({}),
|
|
672
775
|
outputSchema: ListItemsOutputSchema,
|
|
673
|
-
annotations:
|
|
776
|
+
annotations: readOnlyTool('List Themes'),
|
|
674
777
|
}, async () => {
|
|
675
778
|
const structured = { items: [...CAPABILITIES.themes] };
|
|
676
779
|
return {
|
|
@@ -683,7 +786,7 @@ export function createMcpServer(opts = {}) {
|
|
|
683
786
|
description: 'List built-in capacity-icon names usable in the `capacity-icon:` style property.',
|
|
684
787
|
inputSchema: z.object({}),
|
|
685
788
|
outputSchema: ListItemsOutputSchema,
|
|
686
|
-
annotations:
|
|
789
|
+
annotations: readOnlyTool('List Icons'),
|
|
687
790
|
}, async () => {
|
|
688
791
|
const structured = { items: [...CAPABILITIES.icons] };
|
|
689
792
|
return {
|
|
@@ -696,7 +799,7 @@ export function createMcpServer(opts = {}) {
|
|
|
696
799
|
description: 'List supported BCP-47 locale tags.',
|
|
697
800
|
inputSchema: z.object({}),
|
|
698
801
|
outputSchema: ListItemsOutputSchema,
|
|
699
|
-
annotations:
|
|
802
|
+
annotations: readOnlyTool('List Locales'),
|
|
700
803
|
}, async () => {
|
|
701
804
|
const structured = { items: [...CAPABILITIES.locales] };
|
|
702
805
|
return {
|
|
@@ -709,7 +812,7 @@ export function createMcpServer(opts = {}) {
|
|
|
709
812
|
description: 'List all supported export formats (svg, png, pdf, html, mermaid, xlsx, msproj, json).',
|
|
710
813
|
inputSchema: z.object({}),
|
|
711
814
|
outputSchema: ListItemsOutputSchema,
|
|
712
|
-
annotations:
|
|
815
|
+
annotations: readOnlyTool('List Export Formats'),
|
|
713
816
|
}, async () => {
|
|
714
817
|
const structured = { items: [...CAPABILITIES.formats] };
|
|
715
818
|
return {
|
|
@@ -722,7 +825,7 @@ export function createMcpServer(opts = {}) {
|
|
|
722
825
|
description: 'List built-in template names usable with `nowline --init --template`.',
|
|
723
826
|
inputSchema: z.object({}),
|
|
724
827
|
outputSchema: ListItemsOutputSchema,
|
|
725
|
-
annotations:
|
|
828
|
+
annotations: readOnlyTool('List Templates'),
|
|
726
829
|
}, async () => {
|
|
727
830
|
const structured = { items: [...CAPABILITIES.templates] };
|
|
728
831
|
return {
|
|
@@ -740,7 +843,7 @@ export function createMcpServer(opts = {}) {
|
|
|
740
843
|
.describe('Reference format. Defaults to condensed.'),
|
|
741
844
|
}),
|
|
742
845
|
outputSchema: ReferenceOutputSchema,
|
|
743
|
-
annotations:
|
|
846
|
+
annotations: readOnlyTool('View Roadmap Reference'),
|
|
744
847
|
}, async (args) => {
|
|
745
848
|
const format = args.format ?? 'condensed';
|
|
746
849
|
const text = format === 'full' ? REFERENCE_MAN_PAGE : REFERENCE_CHEATSHEET;
|
|
@@ -759,7 +862,7 @@ export function createMcpServer(opts = {}) {
|
|
|
759
862
|
.describe('Example name. Omit for the catalog plus minimal inline.'),
|
|
760
863
|
}),
|
|
761
864
|
outputSchema: ExamplesOutputSchema,
|
|
762
|
-
annotations:
|
|
865
|
+
annotations: readOnlyTool('View Roadmap Examples'),
|
|
763
866
|
}, async (args) => {
|
|
764
867
|
const exampleNames = EXAMPLES.map((e) => exampleShortName(e.name));
|
|
765
868
|
if (args.name) {
|
|
@@ -804,7 +907,7 @@ export function createMcpServer(opts = {}) {
|
|
|
804
907
|
description: 'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties).',
|
|
805
908
|
inputSchema: z.object({}),
|
|
806
909
|
outputSchema: SchemaOutputSchema,
|
|
807
|
-
annotations:
|
|
910
|
+
annotations: readOnlyTool('View Roadmap Schema'),
|
|
808
911
|
}, async () => {
|
|
809
912
|
const structured = {
|
|
810
913
|
directiveKeys: [...SCHEMA_VOCABULARY.directiveKeys],
|