@nowline/mcp 0.6.0 → 0.7.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 +9 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +17 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/generated/resources.d.ts +2 -0
- package/dist/generated/resources.d.ts.map +1 -1
- package/dist/generated/resources.js +6 -1
- package/dist/generated/resources.js.map +1 -1
- package/dist/generated/ui-bundle.d.ts +3 -0
- package/dist/generated/ui-bundle.d.ts.map +1 -0
- package/dist/generated/ui-bundle.js +10 -0
- package/dist/generated/ui-bundle.js.map +1 -0
- package/dist/index.js +63 -12
- package/dist/index.js.map +1 -1
- package/dist/prompts.d.ts +3 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +143 -0
- package/dist/prompts.js.map +1 -0
- package/dist/schemas.d.ts +96 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +65 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +304 -41
- package/dist/server.js.map +1 -1
- package/dist/ui/entry.d.ts +2 -0
- package/dist/ui/entry.d.ts.map +1 -0
- package/dist/ui/entry.js +149 -0
- package/dist/ui/entry.js.map +1 -0
- package/package.json +11 -6
- package/src/capabilities.ts +25 -0
- package/src/generated/resources.ts +7 -1
- package/src/generated/ui-bundle.ts +10 -0
- package/src/index.ts +75 -13
- package/src/prompts.ts +172 -0
- package/src/schemas.ts +79 -0
- package/src/server.ts +406 -39
- package/src/ui/entry.ts +169 -0
package/src/server.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
// @nowline/mcp server factory.
|
|
2
2
|
//
|
|
3
|
-
// Creates and configures an McpServer instance with all
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// paths expose an identical surface.
|
|
3
|
+
// Creates and configures an McpServer instance with all tools, resources, and
|
|
4
|
+
// prompts. The factory is shared by the entry-point bin (npx @nowline/mcp)
|
|
5
|
+
// and the CLI's --mcp flag so both paths expose an identical surface.
|
|
7
6
|
//
|
|
8
|
-
// Spec: specs/mcp.md.
|
|
7
|
+
// Spec: specs/mcp.md.
|
|
9
8
|
|
|
10
9
|
import { promises as fs } from 'node:fs';
|
|
11
10
|
import * as path from 'node:path';
|
|
12
11
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
13
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
collectDocumentDiagnostics,
|
|
14
|
+
createNowlineServices,
|
|
15
|
+
type NowlineFile,
|
|
16
|
+
parseNowlineJson,
|
|
17
|
+
printNowlineFile,
|
|
18
|
+
} from '@nowline/core';
|
|
14
19
|
import {
|
|
15
20
|
type ExportFormat,
|
|
16
21
|
exportDocument,
|
|
@@ -18,9 +23,101 @@ import {
|
|
|
18
23
|
type RenderInputs,
|
|
19
24
|
} from '@nowline/export';
|
|
20
25
|
import { resolveFonts } from '@nowline/export-core';
|
|
26
|
+
import { buildShareLink } from '@nowline/share-link';
|
|
21
27
|
import { URI } from 'langium';
|
|
22
28
|
import { z } from 'zod';
|
|
23
|
-
import {
|
|
29
|
+
import { CAPABILITIES } from './capabilities.js';
|
|
30
|
+
import { CONVERSIONS_GUIDE, EXAMPLES, REFERENCE_MAN_PAGE } from './generated/resources.js';
|
|
31
|
+
import { UI_BUNDLE } from './generated/ui-bundle.js';
|
|
32
|
+
import { registerPrompts } from './prompts.js';
|
|
33
|
+
import {
|
|
34
|
+
CapabilitiesOutputSchema,
|
|
35
|
+
ConvertOutputSchema,
|
|
36
|
+
CreateOutputSchema,
|
|
37
|
+
DeleteOutputSchema,
|
|
38
|
+
ExportOutputSchema,
|
|
39
|
+
ListItemsOutputSchema,
|
|
40
|
+
ListOutputSchema,
|
|
41
|
+
ReadOutputSchema,
|
|
42
|
+
RenderOutputSchema,
|
|
43
|
+
UpdateOutputSchema,
|
|
44
|
+
ValidateOutputSchema,
|
|
45
|
+
} from './schemas.js';
|
|
46
|
+
|
|
47
|
+
// ---- MCP Apps UI (in-chat live preview) -------------------------------------
|
|
48
|
+
//
|
|
49
|
+
// The MCP Apps extension (SEP-1865) lets a tool return an interactive HTML
|
|
50
|
+
// resource the host renders in a sandboxed iframe. We use the embedded-resource
|
|
51
|
+
// form: `render` returns a self-contained text/html resource that inlines the
|
|
52
|
+
// browser preview bundle (UI_BUNDLE, the @nowline/browser + @nowline/preview-
|
|
53
|
+
// shell pipeline) plus the injected source. It is emitted only when the client
|
|
54
|
+
// advertises the UI extension capability or the caller passes `preview: true`,
|
|
55
|
+
// so plain stdio operation is unchanged and non-UI hosts still receive the
|
|
56
|
+
// SVG/PNG content block alongside it (graceful degradation).
|
|
57
|
+
|
|
58
|
+
/** SEP-1865 UI extension capability id; also probed under common short keys. */
|
|
59
|
+
const MCP_APPS_UI_CAPABILITY = 'io.modelcontextprotocol/ui';
|
|
60
|
+
const PREVIEW_UI_URI = 'ui://nowline/preview';
|
|
61
|
+
/** SEP-1865 mandates the text/html;profile=mcp-app media type for UI resources. */
|
|
62
|
+
const PREVIEW_UI_MIME = 'text/html;profile=mcp-app';
|
|
63
|
+
|
|
64
|
+
function clientSupportsAppsUi(server: McpServer): boolean {
|
|
65
|
+
// Extension capabilities are negotiated under `experimental` in SDK 1.29
|
|
66
|
+
// (it does not yet model SEP-1724 extensions as a first-class field), so
|
|
67
|
+
// probe the canonical id plus the short `ui` / `apps` aliases some hosts use.
|
|
68
|
+
const experimental = server.server.getClientCapabilities()?.experimental as
|
|
69
|
+
| Record<string, unknown>
|
|
70
|
+
| undefined;
|
|
71
|
+
if (!experimental) return false;
|
|
72
|
+
return Boolean(experimental[MCP_APPS_UI_CAPABILITY] || experimental.ui || experimental.apps);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface PreviewPayload {
|
|
76
|
+
source: string;
|
|
77
|
+
theme?: string;
|
|
78
|
+
now?: string;
|
|
79
|
+
width?: number;
|
|
80
|
+
locale?: string;
|
|
81
|
+
showLinks?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildPreviewHtml(payload: PreviewPayload): string {
|
|
85
|
+
// The payload (including the .nowline source) is injected as a JSON
|
|
86
|
+
// <script> block rather than interpolated into executable JS, so source
|
|
87
|
+
// text with quotes/backticks can't break out. Escaping `<` as \u003c keeps
|
|
88
|
+
// any embedded "</script>" from closing the block early; JSON.parse in the
|
|
89
|
+
// bundle decodes it back. The bundle injects its own stylesheet at runtime,
|
|
90
|
+
// so only the root-element sizing CSS is inlined here.
|
|
91
|
+
const data = JSON.stringify(payload).replace(/</g, '\\u003c');
|
|
92
|
+
return [
|
|
93
|
+
'<!doctype html>',
|
|
94
|
+
'<html lang="en">',
|
|
95
|
+
'<head>',
|
|
96
|
+
'<meta charset="utf-8" />',
|
|
97
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
98
|
+
'<title>Nowline preview</title>',
|
|
99
|
+
'<style>html,body,#nl-preview-root{margin:0;padding:0;height:100%;width:100%;overflow:hidden;}</style>',
|
|
100
|
+
'</head>',
|
|
101
|
+
'<body>',
|
|
102
|
+
'<div id="nl-preview-root"></div>',
|
|
103
|
+
`<script id="nl-preview-data" type="application/json">${data}</script>`,
|
|
104
|
+
`<script>${UI_BUNDLE}</script>`,
|
|
105
|
+
'</body>',
|
|
106
|
+
'</html>',
|
|
107
|
+
'',
|
|
108
|
+
].join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function previewResourceBlock(payload: PreviewPayload) {
|
|
112
|
+
return {
|
|
113
|
+
type: 'resource' as const,
|
|
114
|
+
resource: {
|
|
115
|
+
uri: PREVIEW_UI_URI,
|
|
116
|
+
mimeType: PREVIEW_UI_MIME,
|
|
117
|
+
text: buildPreviewHtml(payload),
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
24
121
|
|
|
25
122
|
// ---- Diagnostic helpers -----------------------------------------------------
|
|
26
123
|
|
|
@@ -114,7 +211,6 @@ function createNodeHostEnv(sourcePath: string): HostEnv {
|
|
|
114
211
|
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
115
212
|
},
|
|
116
213
|
async loadWasm(): Promise<ArrayBuffer> {
|
|
117
|
-
// Resolve resvg.wasm relative to @nowline/export-png/dist/ at runtime.
|
|
118
214
|
const { createRequire } = await import('node:module');
|
|
119
215
|
const req = createRequire(import.meta.url);
|
|
120
216
|
const entry = req.resolve('@resvg/resvg-wasm');
|
|
@@ -163,7 +259,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
163
259
|
const allowedRoot = opts.allowedRoot ?? process.cwd();
|
|
164
260
|
const server = new McpServer({
|
|
165
261
|
name: opts.name ?? 'nowline',
|
|
166
|
-
version: opts.version ?? '0.
|
|
262
|
+
version: opts.version ?? '0.6.0',
|
|
167
263
|
});
|
|
168
264
|
|
|
169
265
|
// ---- Resources ----------------------------------------------------------
|
|
@@ -199,6 +295,29 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
199
295
|
}),
|
|
200
296
|
);
|
|
201
297
|
|
|
298
|
+
server.registerResource(
|
|
299
|
+
'nowline-conversions',
|
|
300
|
+
'nowline://conversions',
|
|
301
|
+
{
|
|
302
|
+
description:
|
|
303
|
+
'LLM-mediated conversion guide: how to translate Mermaid gantt, MS Project, Excel, Google Sheets timeline, and generic CSV into Nowline DSL.',
|
|
304
|
+
mimeType: 'text/plain',
|
|
305
|
+
},
|
|
306
|
+
async () => ({
|
|
307
|
+
contents: [
|
|
308
|
+
{
|
|
309
|
+
uri: 'nowline://conversions',
|
|
310
|
+
text: CONVERSIONS_GUIDE,
|
|
311
|
+
mimeType: 'text/plain',
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
}),
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// ---- Prompts ------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
registerPrompts(server);
|
|
320
|
+
|
|
202
321
|
// ---- validate -----------------------------------------------------------
|
|
203
322
|
|
|
204
323
|
server.registerTool(
|
|
@@ -213,14 +332,18 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
213
332
|
.optional()
|
|
214
333
|
.describe('Absolute or relative path to a .nowline file to validate.'),
|
|
215
334
|
}),
|
|
335
|
+
outputSchema: ValidateOutputSchema,
|
|
336
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
216
337
|
},
|
|
217
338
|
async (args) => {
|
|
218
339
|
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
219
340
|
const doc = await buildDocument(source);
|
|
220
341
|
const diagnostics = collectMcpDiagnostics(doc, filePath);
|
|
221
342
|
const ok = diagnostics.every((d) => d.severity !== 'error');
|
|
343
|
+
const structured = { ok, diagnostics };
|
|
222
344
|
return {
|
|
223
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
345
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
346
|
+
structuredContent: structured,
|
|
224
347
|
};
|
|
225
348
|
},
|
|
226
349
|
);
|
|
@@ -236,17 +359,16 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
236
359
|
.string()
|
|
237
360
|
.describe('Absolute or relative path to the .nowline file to read.'),
|
|
238
361
|
}),
|
|
362
|
+
outputSchema: ReadOutputSchema,
|
|
363
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
239
364
|
},
|
|
240
365
|
async (args) => {
|
|
241
366
|
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
242
367
|
const source = await fs.readFile(abs, 'utf-8');
|
|
368
|
+
const structured = { path: abs, source };
|
|
243
369
|
return {
|
|
244
|
-
content: [
|
|
245
|
-
|
|
246
|
-
type: 'text',
|
|
247
|
-
text: JSON.stringify({ path: abs, source }, null, 2),
|
|
248
|
-
},
|
|
249
|
-
],
|
|
370
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
371
|
+
structuredContent: structured,
|
|
250
372
|
};
|
|
251
373
|
},
|
|
252
374
|
);
|
|
@@ -262,6 +384,9 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
262
384
|
path: z.string().describe('Absolute or relative path to write the .nowline file.'),
|
|
263
385
|
source: z.string().describe('The .nowline source text to write.'),
|
|
264
386
|
}),
|
|
387
|
+
outputSchema: CreateOutputSchema,
|
|
388
|
+
// Overwrites silently → destructive; same source always produces same file → idempotent.
|
|
389
|
+
annotations: { destructiveHint: true, idempotentHint: true },
|
|
265
390
|
},
|
|
266
391
|
async (args) => {
|
|
267
392
|
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
@@ -281,8 +406,10 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
281
406
|
}
|
|
282
407
|
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
283
408
|
await fs.writeFile(abs, args.source, 'utf-8');
|
|
409
|
+
const structured = { ok: true, path: abs };
|
|
284
410
|
return {
|
|
285
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
411
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
412
|
+
structuredContent: structured,
|
|
286
413
|
};
|
|
287
414
|
},
|
|
288
415
|
);
|
|
@@ -299,6 +426,8 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
299
426
|
.describe('Absolute or relative path of the .nowline file to update.'),
|
|
300
427
|
source: z.string().describe('The new .nowline source text.'),
|
|
301
428
|
}),
|
|
429
|
+
outputSchema: UpdateOutputSchema,
|
|
430
|
+
annotations: { idempotentHint: true },
|
|
302
431
|
},
|
|
303
432
|
async (args) => {
|
|
304
433
|
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
@@ -317,8 +446,10 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
317
446
|
};
|
|
318
447
|
}
|
|
319
448
|
await fs.writeFile(abs, args.source, 'utf-8');
|
|
449
|
+
const structured = { ok: true, path: abs };
|
|
320
450
|
return {
|
|
321
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
451
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
452
|
+
structuredContent: structured,
|
|
322
453
|
};
|
|
323
454
|
},
|
|
324
455
|
);
|
|
@@ -334,12 +465,16 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
334
465
|
.string()
|
|
335
466
|
.describe('Absolute or relative path of the .nowline file to delete.'),
|
|
336
467
|
}),
|
|
468
|
+
outputSchema: DeleteOutputSchema,
|
|
469
|
+
annotations: { destructiveHint: true },
|
|
337
470
|
},
|
|
338
471
|
async (args) => {
|
|
339
472
|
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
340
473
|
await fs.unlink(abs);
|
|
474
|
+
const structured = { path: abs };
|
|
341
475
|
return {
|
|
342
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
476
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
477
|
+
structuredContent: structured,
|
|
343
478
|
};
|
|
344
479
|
},
|
|
345
480
|
);
|
|
@@ -362,13 +497,17 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
362
497
|
.optional()
|
|
363
498
|
.describe('Whether to scan subdirectories. Defaults to false.'),
|
|
364
499
|
}),
|
|
500
|
+
outputSchema: ListOutputSchema,
|
|
501
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
365
502
|
},
|
|
366
503
|
async (args) => {
|
|
367
504
|
const dir = args.directory ? resolveAndGuard(args.directory, allowedRoot) : allowedRoot;
|
|
368
505
|
const recursive = args.recursive ?? false;
|
|
369
506
|
const paths = await listNowlineFiles(dir, recursive);
|
|
507
|
+
const structured = { paths };
|
|
370
508
|
return {
|
|
371
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
509
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
510
|
+
structuredContent: structured,
|
|
372
511
|
};
|
|
373
512
|
},
|
|
374
513
|
);
|
|
@@ -404,7 +543,21 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
404
543
|
.string()
|
|
405
544
|
.optional()
|
|
406
545
|
.describe('Write output to this path instead of returning inline.'),
|
|
546
|
+
share: z
|
|
547
|
+
.boolean()
|
|
548
|
+
.optional()
|
|
549
|
+
.describe(
|
|
550
|
+
'When true, include a shareUrl pointing to https://free.nowline.io/open.',
|
|
551
|
+
),
|
|
552
|
+
preview: z
|
|
553
|
+
.boolean()
|
|
554
|
+
.optional()
|
|
555
|
+
.describe(
|
|
556
|
+
'When true, also return an interactive in-chat HTML preview (MCP Apps UI). Auto-enabled when the client advertises MCP Apps UI support.',
|
|
557
|
+
),
|
|
407
558
|
}),
|
|
559
|
+
outputSchema: RenderOutputSchema,
|
|
560
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
408
561
|
},
|
|
409
562
|
async (args) => {
|
|
410
563
|
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
@@ -424,22 +577,42 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
424
577
|
}
|
|
425
578
|
const host = createNodeHostEnv(filePath);
|
|
426
579
|
const bytes = await exportDocument(source, format, inputs, host);
|
|
580
|
+
const shareUrl = args.share
|
|
581
|
+
? (buildShareLink({ source, share: true }) ?? undefined)
|
|
582
|
+
: undefined;
|
|
583
|
+
|
|
584
|
+
// Optional MCP Apps in-chat preview. Emitted alongside the normal
|
|
585
|
+
// content so non-UI hosts still get the SVG/PNG; the bundle renders
|
|
586
|
+
// the live SVG itself, so the preview is format-agnostic.
|
|
587
|
+
const wantPreview = args.preview === true || clientSupportsAppsUi(server);
|
|
588
|
+
const previewBlocks = wantPreview
|
|
589
|
+
? [
|
|
590
|
+
previewResourceBlock({
|
|
591
|
+
source,
|
|
592
|
+
theme: args.theme,
|
|
593
|
+
now: args.now,
|
|
594
|
+
width: args.width,
|
|
595
|
+
locale: 'en-US',
|
|
596
|
+
}),
|
|
597
|
+
]
|
|
598
|
+
: [];
|
|
427
599
|
|
|
428
600
|
if (args.output) {
|
|
429
601
|
const outAbs = resolveAndGuard(args.output, allowedRoot);
|
|
430
602
|
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
431
603
|
await fs.writeFile(outAbs, bytes);
|
|
604
|
+
const structured = { format, path: outAbs, bytes: bytes.byteLength, shareUrl };
|
|
432
605
|
return {
|
|
433
606
|
content: [
|
|
434
|
-
{
|
|
435
|
-
|
|
436
|
-
text: JSON.stringify({ path: outAbs, bytes: bytes.byteLength }),
|
|
437
|
-
},
|
|
607
|
+
{ type: 'text', text: JSON.stringify(structured, null, 2) },
|
|
608
|
+
...previewBlocks,
|
|
438
609
|
],
|
|
610
|
+
structuredContent: structured,
|
|
439
611
|
};
|
|
440
612
|
}
|
|
441
613
|
|
|
442
614
|
if (format === 'png') {
|
|
615
|
+
const structured = { format, bytes: bytes.byteLength, shareUrl };
|
|
443
616
|
return {
|
|
444
617
|
content: [
|
|
445
618
|
{
|
|
@@ -447,17 +620,19 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
447
620
|
data: Buffer.from(bytes).toString('base64'),
|
|
448
621
|
mimeType: 'image/png',
|
|
449
622
|
},
|
|
623
|
+
...previewBlocks,
|
|
450
624
|
],
|
|
625
|
+
structuredContent: structured,
|
|
451
626
|
};
|
|
452
627
|
}
|
|
628
|
+
const svgText = new TextDecoder('utf-8').decode(bytes);
|
|
629
|
+
const structured = { format, shareUrl };
|
|
453
630
|
return {
|
|
454
631
|
content: [
|
|
455
|
-
{
|
|
456
|
-
|
|
457
|
-
text: new TextDecoder('utf-8').decode(bytes),
|
|
458
|
-
mimeType: 'image/svg+xml',
|
|
459
|
-
},
|
|
632
|
+
{ type: 'text', text: svgText, mimeType: 'image/svg+xml' },
|
|
633
|
+
...previewBlocks,
|
|
460
634
|
],
|
|
635
|
+
structuredContent: structured,
|
|
461
636
|
};
|
|
462
637
|
},
|
|
463
638
|
);
|
|
@@ -495,7 +670,15 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
495
670
|
.string()
|
|
496
671
|
.optional()
|
|
497
672
|
.describe('MS Project start date override (YYYY-MM-DD).'),
|
|
673
|
+
share: z
|
|
674
|
+
.boolean()
|
|
675
|
+
.optional()
|
|
676
|
+
.describe(
|
|
677
|
+
'When true, include a shareUrl pointing to https://free.nowline.io/open.',
|
|
678
|
+
),
|
|
498
679
|
}),
|
|
680
|
+
outputSchema: ExportOutputSchema,
|
|
681
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
499
682
|
},
|
|
500
683
|
async (args) => {
|
|
501
684
|
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
@@ -518,6 +701,9 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
518
701
|
}
|
|
519
702
|
const host = createNodeHostEnv(filePath);
|
|
520
703
|
const bytes = await exportDocument(source, format, inputs, host);
|
|
704
|
+
const shareUrl = args.share
|
|
705
|
+
? (buildShareLink({ source, share: true }) ?? undefined)
|
|
706
|
+
: undefined;
|
|
521
707
|
|
|
522
708
|
const BINARY_FORMATS = new Set<ExportFormat>(['png', 'pdf', 'xlsx']);
|
|
523
709
|
const isBinary = BINARY_FORMATS.has(format);
|
|
@@ -526,13 +712,10 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
526
712
|
const outAbs = resolveAndGuard(args.output, allowedRoot);
|
|
527
713
|
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
528
714
|
await fs.writeFile(outAbs, bytes);
|
|
715
|
+
const structured = { format, path: outAbs, bytes: bytes.byteLength, shareUrl };
|
|
529
716
|
return {
|
|
530
|
-
content: [
|
|
531
|
-
|
|
532
|
-
type: 'text',
|
|
533
|
-
text: JSON.stringify({ path: outAbs, bytes: bytes.byteLength }),
|
|
534
|
-
},
|
|
535
|
-
],
|
|
717
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
718
|
+
structuredContent: structured,
|
|
536
719
|
};
|
|
537
720
|
}
|
|
538
721
|
|
|
@@ -542,6 +725,7 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
542
725
|
pdf: 'application/pdf',
|
|
543
726
|
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
544
727
|
};
|
|
728
|
+
const structured = { format, bytes: bytes.byteLength, shareUrl };
|
|
545
729
|
return {
|
|
546
730
|
content: [
|
|
547
731
|
{
|
|
@@ -550,15 +734,198 @@ export function createMcpServer(opts: McpServerOptions = {}): McpServer {
|
|
|
550
734
|
mimeType: mimeMap[format] ?? 'application/octet-stream',
|
|
551
735
|
},
|
|
552
736
|
],
|
|
737
|
+
structuredContent: structured,
|
|
553
738
|
};
|
|
554
739
|
}
|
|
740
|
+
const text = new TextDecoder('utf-8').decode(bytes);
|
|
741
|
+
const structured = { format, shareUrl };
|
|
555
742
|
return {
|
|
556
|
-
content: [
|
|
743
|
+
content: [{ type: 'text', text }],
|
|
744
|
+
structuredContent: structured,
|
|
745
|
+
};
|
|
746
|
+
},
|
|
747
|
+
);
|
|
748
|
+
|
|
749
|
+
// ---- convert ------------------------------------------------------------
|
|
750
|
+
|
|
751
|
+
server.registerTool(
|
|
752
|
+
'convert',
|
|
753
|
+
{
|
|
754
|
+
description:
|
|
755
|
+
'Convert between .nowline source text and its JSON AST representation. `to:json` serializes a .nowline file to the JSON AST; `to:nowline` pretty-prints a JSON AST back to canonical .nowline source.',
|
|
756
|
+
inputSchema: z.object({
|
|
757
|
+
source: z.string().optional().describe('Inline source text to convert.'),
|
|
758
|
+
path: z.string().optional().describe('Path to the source file.'),
|
|
759
|
+
to: z
|
|
760
|
+
.enum(['json', 'nowline'])
|
|
761
|
+
.describe(
|
|
762
|
+
'"json" — serialize .nowline text to JSON AST. "nowline" — pretty-print a JSON AST back to .nowline source.',
|
|
763
|
+
),
|
|
764
|
+
}),
|
|
765
|
+
outputSchema: ConvertOutputSchema,
|
|
766
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
767
|
+
},
|
|
768
|
+
async (args) => {
|
|
769
|
+
if (args.to === 'json') {
|
|
770
|
+
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
771
|
+
const host = createNodeHostEnv(filePath);
|
|
772
|
+
const jsonBytes = await exportDocument(
|
|
773
|
+
source,
|
|
774
|
+
'json',
|
|
557
775
|
{
|
|
558
|
-
|
|
559
|
-
|
|
776
|
+
sourcePath: filePath,
|
|
777
|
+
today: todayUtc(),
|
|
778
|
+
locale: 'en-US',
|
|
779
|
+
theme: 'light',
|
|
560
780
|
},
|
|
561
|
-
|
|
781
|
+
host,
|
|
782
|
+
);
|
|
783
|
+
const result = new TextDecoder('utf-8').decode(jsonBytes);
|
|
784
|
+
const structured = { to: 'json' as const, result };
|
|
785
|
+
return {
|
|
786
|
+
content: [{ type: 'text', text: result }],
|
|
787
|
+
structuredContent: structured,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// to: 'nowline' — input is a JSON AST string
|
|
792
|
+
const jsonSource =
|
|
793
|
+
args.source ??
|
|
794
|
+
(args.path
|
|
795
|
+
? await fs.readFile(resolveAndGuard(args.path, allowedRoot), 'utf-8')
|
|
796
|
+
: null);
|
|
797
|
+
if (!jsonSource) {
|
|
798
|
+
throw new Error('At least one of `source` or `path` is required.');
|
|
799
|
+
}
|
|
800
|
+
const { ast } = parseNowlineJson(jsonSource, args.path ?? 'input.json');
|
|
801
|
+
const result = printNowlineFile(ast);
|
|
802
|
+
const structured = { to: 'nowline' as const, result };
|
|
803
|
+
return {
|
|
804
|
+
content: [{ type: 'text', text: result }],
|
|
805
|
+
structuredContent: structured,
|
|
806
|
+
};
|
|
807
|
+
},
|
|
808
|
+
);
|
|
809
|
+
|
|
810
|
+
// ---- capabilities -------------------------------------------------------
|
|
811
|
+
|
|
812
|
+
server.registerTool(
|
|
813
|
+
'capabilities',
|
|
814
|
+
{
|
|
815
|
+
description:
|
|
816
|
+
'Return all supported themes, icons, locales, export formats, and template names in a single response.',
|
|
817
|
+
inputSchema: z.object({}),
|
|
818
|
+
outputSchema: CapabilitiesOutputSchema,
|
|
819
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
820
|
+
},
|
|
821
|
+
async () => {
|
|
822
|
+
const structured = {
|
|
823
|
+
themes: [...CAPABILITIES.themes],
|
|
824
|
+
icons: [...CAPABILITIES.icons],
|
|
825
|
+
locales: [...CAPABILITIES.locales],
|
|
826
|
+
formats: [...CAPABILITIES.formats],
|
|
827
|
+
templates: [...CAPABILITIES.templates],
|
|
828
|
+
};
|
|
829
|
+
return {
|
|
830
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
831
|
+
structuredContent: structured,
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
);
|
|
835
|
+
|
|
836
|
+
// ---- list-themes --------------------------------------------------------
|
|
837
|
+
|
|
838
|
+
server.registerTool(
|
|
839
|
+
'list-themes',
|
|
840
|
+
{
|
|
841
|
+
description: 'List supported color themes: light, dark, grayscale.',
|
|
842
|
+
inputSchema: z.object({}),
|
|
843
|
+
outputSchema: ListItemsOutputSchema,
|
|
844
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
845
|
+
},
|
|
846
|
+
async () => {
|
|
847
|
+
const structured = { items: [...CAPABILITIES.themes] };
|
|
848
|
+
return {
|
|
849
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
850
|
+
structuredContent: structured,
|
|
851
|
+
};
|
|
852
|
+
},
|
|
853
|
+
);
|
|
854
|
+
|
|
855
|
+
// ---- list-icons ---------------------------------------------------------
|
|
856
|
+
|
|
857
|
+
server.registerTool(
|
|
858
|
+
'list-icons',
|
|
859
|
+
{
|
|
860
|
+
description:
|
|
861
|
+
'List built-in capacity-icon names usable in the `capacity-icon:` style property.',
|
|
862
|
+
inputSchema: z.object({}),
|
|
863
|
+
outputSchema: ListItemsOutputSchema,
|
|
864
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
865
|
+
},
|
|
866
|
+
async () => {
|
|
867
|
+
const structured = { items: [...CAPABILITIES.icons] };
|
|
868
|
+
return {
|
|
869
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
870
|
+
structuredContent: structured,
|
|
871
|
+
};
|
|
872
|
+
},
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
// ---- list-locales -------------------------------------------------------
|
|
876
|
+
|
|
877
|
+
server.registerTool(
|
|
878
|
+
'list-locales',
|
|
879
|
+
{
|
|
880
|
+
description: 'List supported BCP-47 locale tags.',
|
|
881
|
+
inputSchema: z.object({}),
|
|
882
|
+
outputSchema: ListItemsOutputSchema,
|
|
883
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
884
|
+
},
|
|
885
|
+
async () => {
|
|
886
|
+
const structured = { items: [...CAPABILITIES.locales] };
|
|
887
|
+
return {
|
|
888
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
889
|
+
structuredContent: structured,
|
|
890
|
+
};
|
|
891
|
+
},
|
|
892
|
+
);
|
|
893
|
+
|
|
894
|
+
// ---- list-formats -------------------------------------------------------
|
|
895
|
+
|
|
896
|
+
server.registerTool(
|
|
897
|
+
'list-formats',
|
|
898
|
+
{
|
|
899
|
+
description:
|
|
900
|
+
'List all supported export formats (svg, png, pdf, html, mermaid, xlsx, msproj, json).',
|
|
901
|
+
inputSchema: z.object({}),
|
|
902
|
+
outputSchema: ListItemsOutputSchema,
|
|
903
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
904
|
+
},
|
|
905
|
+
async () => {
|
|
906
|
+
const structured = { items: [...CAPABILITIES.formats] };
|
|
907
|
+
return {
|
|
908
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
909
|
+
structuredContent: structured,
|
|
910
|
+
};
|
|
911
|
+
},
|
|
912
|
+
);
|
|
913
|
+
|
|
914
|
+
// ---- list-templates -----------------------------------------------------
|
|
915
|
+
|
|
916
|
+
server.registerTool(
|
|
917
|
+
'list-templates',
|
|
918
|
+
{
|
|
919
|
+
description: 'List built-in template names usable with `nowline --init --template`.',
|
|
920
|
+
inputSchema: z.object({}),
|
|
921
|
+
outputSchema: ListItemsOutputSchema,
|
|
922
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
923
|
+
},
|
|
924
|
+
async () => {
|
|
925
|
+
const structured = { items: [...CAPABILITIES.templates] };
|
|
926
|
+
return {
|
|
927
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
928
|
+
structuredContent: structured,
|
|
562
929
|
};
|
|
563
930
|
},
|
|
564
931
|
);
|