@nowline/mcp 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/branding.d.ts +20 -0
- package/dist/branding.d.ts.map +1 -0
- package/dist/branding.js +27 -0
- package/dist/branding.js.map +1 -0
- 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/diagnostics.d.ts +59 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +117 -0
- package/dist/diagnostics.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 +5 -0
- package/dist/generated/ui-bundle.d.ts.map +1 -0
- package/dist/generated/ui-bundle.js +11 -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/reference-cheatsheet.d.ts +2 -0
- package/dist/reference-cheatsheet.d.ts.map +1 -0
- package/dist/reference-cheatsheet.js +47 -0
- package/dist/reference-cheatsheet.js.map +1 -0
- package/dist/schema-vocab.d.ts +6 -0
- package/dist/schema-vocab.d.ts.map +1 -0
- package/dist/schema-vocab.js +55 -0
- package/dist/schema-vocab.js.map +1 -0
- package/dist/schemas.d.ts +148 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +88 -0
- package/dist/schemas.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +531 -121
- 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 +187 -0
- package/dist/ui/entry.js.map +1 -0
- package/dist/ui/payload.d.ts +30 -0
- package/dist/ui/payload.d.ts.map +1 -0
- package/dist/ui/payload.js +49 -0
- package/dist/ui/payload.js.map +1 -0
- package/package.json +15 -6
- package/src/branding.ts +26 -0
- package/src/capabilities.ts +25 -0
- package/src/diagnostics.ts +185 -0
- package/src/generated/resources.ts +7 -1
- package/src/generated/ui-bundle.ts +12 -0
- package/src/index.ts +75 -13
- package/src/prompts.ts +172 -0
- package/src/reference-cheatsheet.ts +47 -0
- package/src/schema-vocab.ts +55 -0
- package/src/schemas.ts +106 -0
- package/src/server.ts +725 -139
- package/src/ui/entry.ts +214 -0
- package/src/ui/payload.ts +63 -0
package/dist/server.js
CHANGED
|
@@ -1,65 +1,65 @@
|
|
|
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
|
import { promises as fs } from 'node:fs';
|
|
10
9
|
import * as path from 'node:path';
|
|
10
|
+
import { getUiCapability, RESOURCE_MIME_TYPE, registerAppResource, registerAppTool, } from '@modelcontextprotocol/ext-apps/server';
|
|
11
11
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
-
import {
|
|
12
|
+
import { parseNowlineJson, printNowlineFile } from '@nowline/core';
|
|
13
13
|
import { exportDocument, } from '@nowline/export';
|
|
14
14
|
import { resolveFonts } from '@nowline/export-core';
|
|
15
|
-
import {
|
|
15
|
+
import { buildShareLink } from '@nowline/share-link';
|
|
16
16
|
import { z } from 'zod';
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
let cachedServices;
|
|
49
|
-
let docCounter = 0;
|
|
50
|
-
function getServices() {
|
|
51
|
-
if (!cachedServices)
|
|
52
|
-
cachedServices = createNowlineServices();
|
|
53
|
-
return cachedServices;
|
|
17
|
+
import { NOWLINE_MCP_ICONS } from './branding.js';
|
|
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';
|
|
20
|
+
import { CONVERSIONS_GUIDE, EXAMPLES, REFERENCE_MAN_PAGE, } from './generated/resources.js';
|
|
21
|
+
import { PREVIEW_HTML } from './generated/ui-bundle.js';
|
|
22
|
+
import { registerPrompts } from './prompts.js';
|
|
23
|
+
import { REFERENCE_CHEATSHEET } from './reference-cheatsheet.js';
|
|
24
|
+
import { SCHEMA_VOCABULARY } from './schema-vocab.js';
|
|
25
|
+
import { CapabilitiesOutputSchema, ConvertOutputSchema, CreateOutputSchema, DeleteOutputSchema, ExamplesOutputSchema, ExportOutputSchema, ListItemsOutputSchema, ListOutputSchema, ReadOutputSchema, ReferenceOutputSchema, RenderOutputSchema, SchemaOutputSchema, UpdateOutputSchema, ValidateOutputSchema, } from './schemas.js';
|
|
26
|
+
// ---- MCP Apps UI (in-chat live preview) -------------------------------------
|
|
27
|
+
//
|
|
28
|
+
// Official MCP Apps model (SEP-1865): a pre-declared ui:// resource serves
|
|
29
|
+
// static HTML; the render tool declares _meta.ui.resourceUri; per-call data
|
|
30
|
+
// flows through the ontoolresult handshake. When an MCP Apps host is active,
|
|
31
|
+
// render returns a lean nowline.preview JSON payload (no inline SVG/PNG) so
|
|
32
|
+
// results stay under the host's ~150K inline cap. Non-apps hosts still get
|
|
33
|
+
// the full SVG/PNG inline (graceful degradation).
|
|
34
|
+
/** SEP-1865 UI extension capability id; also probed under common short keys. */
|
|
35
|
+
const MCP_APPS_UI_CAPABILITY = 'io.modelcontextprotocol/ui';
|
|
36
|
+
/** Versioned URI doubles as a cache key — bump suffix on bundle changes. */
|
|
37
|
+
export const PREVIEW_UI_URI = 'ui://nowline/preview-v1';
|
|
38
|
+
function clientSupportsAppsUi(server) {
|
|
39
|
+
// SEP-1724 negotiated extensions: canonical id under `extensions`.
|
|
40
|
+
const caps = server.server.getClientCapabilities();
|
|
41
|
+
if (!caps)
|
|
42
|
+
return false;
|
|
43
|
+
if (getUiCapability(caps))
|
|
44
|
+
return true;
|
|
45
|
+
// Hosts also advertise under `experimental` or short `ui` / `apps` aliases.
|
|
46
|
+
const buckets = [caps.extensions, caps.experimental].filter((bucket) => Boolean(bucket));
|
|
47
|
+
return buckets.some((bucket) => Boolean(bucket[MCP_APPS_UI_CAPABILITY] || bucket.ui || bucket.apps));
|
|
54
48
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
function leanPreviewBlock(payload) {
|
|
50
|
+
return {
|
|
51
|
+
type: 'text',
|
|
52
|
+
text: JSON.stringify({
|
|
53
|
+
kind: 'nowline.preview',
|
|
54
|
+
source: payload.source,
|
|
55
|
+
theme: payload.theme,
|
|
56
|
+
now: payload.now,
|
|
57
|
+
width: payload.width,
|
|
58
|
+
locale: payload.locale,
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
61
|
}
|
|
62
|
-
// ----
|
|
62
|
+
// ---- Server factory ---------------------------------------------------------
|
|
63
63
|
function resolveAndGuard(filePath, allowedRoot) {
|
|
64
64
|
const abs = path.resolve(allowedRoot, filePath);
|
|
65
65
|
const guard = path.resolve(allowedRoot);
|
|
@@ -84,7 +84,6 @@ function createNodeHostEnv(sourcePath) {
|
|
|
84
84
|
return new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
85
85
|
},
|
|
86
86
|
async loadWasm() {
|
|
87
|
-
// Resolve resvg.wasm relative to @nowline/export-png/dist/ at runtime.
|
|
88
87
|
const { createRequire } = await import('node:module');
|
|
89
88
|
const req = createRequire(import.meta.url);
|
|
90
89
|
const entry = req.resolve('@resvg/resvg-wasm');
|
|
@@ -100,6 +99,13 @@ function todayUtc() {
|
|
|
100
99
|
const now = new Date();
|
|
101
100
|
return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
|
|
102
101
|
}
|
|
102
|
+
function exampleShortName(fullName) {
|
|
103
|
+
return fullName.endsWith('.nowline') ? fullName.slice(0, -'.nowline'.length) : fullName;
|
|
104
|
+
}
|
|
105
|
+
function findExample(name) {
|
|
106
|
+
const withExt = name.endsWith('.nowline') ? name : `${name}.nowline`;
|
|
107
|
+
return EXAMPLES.find((e) => e.name === withExt || e.name === name);
|
|
108
|
+
}
|
|
103
109
|
async function sourceAndPath(args, allowedRoot) {
|
|
104
110
|
if (args.source !== undefined && args.path === undefined) {
|
|
105
111
|
return { source: args.source, filePath: path.join(allowedRoot, 'unnamed.nowline') };
|
|
@@ -115,7 +121,15 @@ export function createMcpServer(opts = {}) {
|
|
|
115
121
|
const allowedRoot = opts.allowedRoot ?? process.cwd();
|
|
116
122
|
const server = new McpServer({
|
|
117
123
|
name: opts.name ?? 'nowline',
|
|
118
|
-
version: opts.version ?? '0.
|
|
124
|
+
version: opts.version ?? '0.6.0',
|
|
125
|
+
icons: [...NOWLINE_MCP_ICONS],
|
|
126
|
+
}, {
|
|
127
|
+
instructions: 'Nowline manages roadmaps written in the .nowline plain-text DSL — NOT JSON or any other ' +
|
|
128
|
+
'structured format. All `source` parameters expect `.nowline` DSL text (starts with `nowline v1`). ' +
|
|
129
|
+
'Workflow: 1. call `reference` or `examples` to learn syntax → 2. write `.nowline` → ' +
|
|
130
|
+
'3. call `render` (validates + renders; or `validate` alone) → 4. fix errors keyed on `NL.E####` ' +
|
|
131
|
+
'and re-render → 5. review returned layout `insights` (what reflowed) → 6. when uncertain, ' +
|
|
132
|
+
'call `render` with `review:true` for a final visual check. JSON in `convert` is AST conversion only.',
|
|
119
133
|
});
|
|
120
134
|
// ---- Resources ----------------------------------------------------------
|
|
121
135
|
server.registerResource('nowline-reference', 'nowline://reference', {
|
|
@@ -136,9 +150,34 @@ export function createMcpServer(opts = {}) {
|
|
|
136
150
|
mimeType: 'text/plain',
|
|
137
151
|
})),
|
|
138
152
|
}));
|
|
153
|
+
server.registerResource('nowline-conversions', 'nowline://conversions', {
|
|
154
|
+
description: 'LLM-mediated conversion guide: how to translate Mermaid gantt, MS Project, Excel, Google Sheets timeline, and generic CSV into Nowline DSL.',
|
|
155
|
+
mimeType: 'text/plain',
|
|
156
|
+
}, async () => ({
|
|
157
|
+
contents: [
|
|
158
|
+
{
|
|
159
|
+
uri: 'nowline://conversions',
|
|
160
|
+
text: CONVERSIONS_GUIDE,
|
|
161
|
+
mimeType: 'text/plain',
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
}));
|
|
165
|
+
registerAppResource(server, 'nowline-preview', PREVIEW_UI_URI, {
|
|
166
|
+
description: 'Interactive in-chat roadmap preview (MCP Apps). Hydrates via ontoolresult.',
|
|
167
|
+
}, async () => ({
|
|
168
|
+
contents: [
|
|
169
|
+
{
|
|
170
|
+
uri: PREVIEW_UI_URI,
|
|
171
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
172
|
+
text: PREVIEW_HTML,
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
}));
|
|
176
|
+
// ---- Prompts ------------------------------------------------------------
|
|
177
|
+
registerPrompts(server);
|
|
139
178
|
// ---- validate -----------------------------------------------------------
|
|
140
179
|
server.registerTool('validate', {
|
|
141
|
-
description: 'Parse and validate a .nowline roadmap. Returns ok=true
|
|
180
|
+
description: toolDescriptionWithSyntax('Parse and validate a .nowline roadmap. Returns ok=true with optional layout insights when valid, or ok=false with structured diagnostics.'),
|
|
142
181
|
inputSchema: z.object({
|
|
143
182
|
source: z.string().optional().describe('Inline .nowline source text to validate.'),
|
|
144
183
|
path: z
|
|
@@ -146,13 +185,32 @@ export function createMcpServer(opts = {}) {
|
|
|
146
185
|
.optional()
|
|
147
186
|
.describe('Absolute or relative path to a .nowline file to validate.'),
|
|
148
187
|
}),
|
|
188
|
+
outputSchema: ValidateOutputSchema,
|
|
189
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
149
190
|
}, async (args) => {
|
|
150
191
|
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
151
192
|
const doc = await buildDocument(source);
|
|
152
193
|
const diagnostics = collectMcpDiagnostics(doc, filePath);
|
|
153
194
|
const ok = diagnostics.every((d) => d.severity !== 'error');
|
|
195
|
+
const insights = ok
|
|
196
|
+
? await collectMcpLayoutInsights({
|
|
197
|
+
source,
|
|
198
|
+
filePath,
|
|
199
|
+
today: todayUtc(),
|
|
200
|
+
locale: 'en-US',
|
|
201
|
+
readFile: createNodeHostEnv(filePath).readSource,
|
|
202
|
+
doc,
|
|
203
|
+
})
|
|
204
|
+
: [];
|
|
205
|
+
const structured = { ok, diagnostics, ...(insights.length > 0 ? { insights } : {}) };
|
|
154
206
|
return {
|
|
155
|
-
content: [
|
|
207
|
+
content: [
|
|
208
|
+
{ type: 'text', text: JSON.stringify(structured, null, 2) },
|
|
209
|
+
...(insights.length > 0
|
|
210
|
+
? [{ type: 'text', text: LAYOUT_INSIGHT_HINT }]
|
|
211
|
+
: []),
|
|
212
|
+
],
|
|
213
|
+
structuredContent: structured,
|
|
156
214
|
};
|
|
157
215
|
});
|
|
158
216
|
// ---- read ---------------------------------------------------------------
|
|
@@ -163,75 +221,61 @@ export function createMcpServer(opts = {}) {
|
|
|
163
221
|
.string()
|
|
164
222
|
.describe('Absolute or relative path to the .nowline file to read.'),
|
|
165
223
|
}),
|
|
224
|
+
outputSchema: ReadOutputSchema,
|
|
225
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
166
226
|
}, async (args) => {
|
|
167
227
|
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
168
228
|
const source = await fs.readFile(abs, 'utf-8');
|
|
229
|
+
const structured = { path: abs, source };
|
|
169
230
|
return {
|
|
170
|
-
content: [
|
|
171
|
-
|
|
172
|
-
type: 'text',
|
|
173
|
-
text: JSON.stringify({ path: abs, source }, null, 2),
|
|
174
|
-
},
|
|
175
|
-
],
|
|
231
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
232
|
+
structuredContent: structured,
|
|
176
233
|
};
|
|
177
234
|
});
|
|
178
235
|
// ---- create -------------------------------------------------------------
|
|
179
236
|
server.registerTool('create', {
|
|
180
|
-
description: 'Write a new .nowline file after validation. Overwrites if the path already exists.',
|
|
237
|
+
description: toolDescriptionWithSyntax('Write a new .nowline file after validation. Overwrites if the path already exists.'),
|
|
181
238
|
inputSchema: z.object({
|
|
182
239
|
path: z.string().describe('Absolute or relative path to write the .nowline file.'),
|
|
183
240
|
source: z.string().describe('The .nowline source text to write.'),
|
|
184
241
|
}),
|
|
242
|
+
outputSchema: CreateOutputSchema,
|
|
243
|
+
// Overwrites silently → destructive; same source always produces same file → idempotent.
|
|
244
|
+
annotations: { destructiveHint: true, idempotentHint: true },
|
|
185
245
|
}, async (args) => {
|
|
186
246
|
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (errors.length > 0) {
|
|
191
|
-
return {
|
|
192
|
-
content: [
|
|
193
|
-
{
|
|
194
|
-
type: 'text',
|
|
195
|
-
text: JSON.stringify({ ok: false, path: abs, diagnostics }, null, 2),
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
isError: true,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
247
|
+
const blocked = await diagnosticsErrorBlock(args.source, abs);
|
|
248
|
+
if (!blocked.ok)
|
|
249
|
+
return blocked.response;
|
|
201
250
|
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
202
251
|
await fs.writeFile(abs, args.source, 'utf-8');
|
|
252
|
+
const structured = { ok: true, path: abs };
|
|
203
253
|
return {
|
|
204
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
254
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
255
|
+
structuredContent: structured,
|
|
205
256
|
};
|
|
206
257
|
});
|
|
207
258
|
// ---- update -------------------------------------------------------------
|
|
208
259
|
server.registerTool('update', {
|
|
209
|
-
description: 'Replace an existing .nowline file after validation.',
|
|
260
|
+
description: toolDescriptionWithSyntax('Replace an existing .nowline file after validation.'),
|
|
210
261
|
inputSchema: z.object({
|
|
211
262
|
path: z
|
|
212
263
|
.string()
|
|
213
264
|
.describe('Absolute or relative path of the .nowline file to update.'),
|
|
214
265
|
source: z.string().describe('The new .nowline source text.'),
|
|
215
266
|
}),
|
|
267
|
+
outputSchema: UpdateOutputSchema,
|
|
268
|
+
annotations: { idempotentHint: true },
|
|
216
269
|
}, async (args) => {
|
|
217
270
|
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (errors.length > 0) {
|
|
222
|
-
return {
|
|
223
|
-
content: [
|
|
224
|
-
{
|
|
225
|
-
type: 'text',
|
|
226
|
-
text: JSON.stringify({ ok: false, path: abs, diagnostics }, null, 2),
|
|
227
|
-
},
|
|
228
|
-
],
|
|
229
|
-
isError: true,
|
|
230
|
-
};
|
|
231
|
-
}
|
|
271
|
+
const blocked = await diagnosticsErrorBlock(args.source, abs);
|
|
272
|
+
if (!blocked.ok)
|
|
273
|
+
return blocked.response;
|
|
232
274
|
await fs.writeFile(abs, args.source, 'utf-8');
|
|
275
|
+
const structured = { ok: true, path: abs };
|
|
233
276
|
return {
|
|
234
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
277
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
278
|
+
structuredContent: structured,
|
|
235
279
|
};
|
|
236
280
|
});
|
|
237
281
|
// ---- delete -------------------------------------------------------------
|
|
@@ -242,11 +286,15 @@ export function createMcpServer(opts = {}) {
|
|
|
242
286
|
.string()
|
|
243
287
|
.describe('Absolute or relative path of the .nowline file to delete.'),
|
|
244
288
|
}),
|
|
289
|
+
outputSchema: DeleteOutputSchema,
|
|
290
|
+
annotations: { destructiveHint: true },
|
|
245
291
|
}, async (args) => {
|
|
246
292
|
const abs = resolveAndGuard(args.path, allowedRoot);
|
|
247
293
|
await fs.unlink(abs);
|
|
294
|
+
const structured = { path: abs };
|
|
248
295
|
return {
|
|
249
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
296
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
297
|
+
structuredContent: structured,
|
|
250
298
|
};
|
|
251
299
|
});
|
|
252
300
|
// ---- list ---------------------------------------------------------------
|
|
@@ -262,20 +310,30 @@ export function createMcpServer(opts = {}) {
|
|
|
262
310
|
.optional()
|
|
263
311
|
.describe('Whether to scan subdirectories. Defaults to false.'),
|
|
264
312
|
}),
|
|
313
|
+
outputSchema: ListOutputSchema,
|
|
314
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
265
315
|
}, async (args) => {
|
|
266
316
|
const dir = args.directory ? resolveAndGuard(args.directory, allowedRoot) : allowedRoot;
|
|
267
317
|
const recursive = args.recursive ?? false;
|
|
268
318
|
const paths = await listNowlineFiles(dir, recursive);
|
|
319
|
+
const structured = { paths };
|
|
269
320
|
return {
|
|
270
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
321
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
322
|
+
structuredContent: structured,
|
|
271
323
|
};
|
|
272
324
|
});
|
|
273
325
|
// ---- render -------------------------------------------------------------
|
|
274
|
-
server
|
|
275
|
-
description: '
|
|
326
|
+
registerAppTool(server, 'render', {
|
|
327
|
+
description: toolDescriptionWithSyntax('Validate then render a .nowline roadmap to SVG or PNG (combined validate+render+share). ' +
|
|
328
|
+
'Returns structured diagnostics on error-severity input instead of a raw kernel error.'),
|
|
276
329
|
inputSchema: z.object({
|
|
277
330
|
source: z.string().optional().describe('Inline .nowline source text.'),
|
|
278
|
-
path: z
|
|
331
|
+
path: z
|
|
332
|
+
.string()
|
|
333
|
+
.optional()
|
|
334
|
+
.describe('Real local filesystem path to the .nowline file (e.g. /Users/name/Desktop/foo.nowline). ' +
|
|
335
|
+
'Never pass a virtual or sandbox path such as /mnt/user-data/… — ' +
|
|
336
|
+
'those do not exist on the host filesystem. Pass `source` instead.'),
|
|
279
337
|
format: z
|
|
280
338
|
.enum(['svg', 'png'])
|
|
281
339
|
.optional()
|
|
@@ -296,10 +354,33 @@ export function createMcpServer(opts = {}) {
|
|
|
296
354
|
output: z
|
|
297
355
|
.string()
|
|
298
356
|
.optional()
|
|
299
|
-
.describe('
|
|
357
|
+
.describe('Real local filesystem path to write the output file (e.g. /Users/name/Desktop/roadmap.svg). ' +
|
|
358
|
+
'Never pass a virtual or sandbox path such as /mnt/user-data/… — ' +
|
|
359
|
+
'omit this parameter to receive output inline instead.'),
|
|
360
|
+
share: z
|
|
361
|
+
.boolean()
|
|
362
|
+
.optional()
|
|
363
|
+
.describe('When true, include a shareUrl pointing to https://free.nowline.io/open.'),
|
|
364
|
+
review: z
|
|
365
|
+
.boolean()
|
|
366
|
+
.optional()
|
|
367
|
+
.describe('When true, also attach a downscaled PNG so a multimodal model can visually check layout (label overflow, lane crowding, off-range now-line). Off by default.'),
|
|
368
|
+
preview: z
|
|
369
|
+
.boolean()
|
|
370
|
+
.optional()
|
|
371
|
+
.describe('When true, force the in-chat MCP Apps preview. On MCP Apps hosts the preview auto-renders via _meta.ui without this flag.'),
|
|
300
372
|
}),
|
|
373
|
+
outputSchema: RenderOutputSchema,
|
|
374
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
375
|
+
_meta: {
|
|
376
|
+
ui: { resourceUri: PREVIEW_UI_URI },
|
|
377
|
+
'openai/outputTemplate': PREVIEW_UI_URI,
|
|
378
|
+
},
|
|
301
379
|
}, async (args) => {
|
|
302
380
|
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
381
|
+
const blocked = await diagnosticsErrorBlock(source, filePath);
|
|
382
|
+
if (!blocked.ok)
|
|
383
|
+
return blocked.response;
|
|
303
384
|
const format = args.format ?? 'svg';
|
|
304
385
|
const today = args.now ? new Date(`${args.now}T00:00:00Z`) : todayUtc();
|
|
305
386
|
const inputs = {
|
|
@@ -310,26 +391,81 @@ export function createMcpServer(opts = {}) {
|
|
|
310
391
|
width: args.width,
|
|
311
392
|
pngScale: args.scale,
|
|
312
393
|
};
|
|
313
|
-
|
|
394
|
+
const host = createNodeHostEnv(filePath);
|
|
395
|
+
const appActive = args.preview === true || clientSupportsAppsUi(server);
|
|
396
|
+
// Skip the full render when the bytes won't be used: apps host with
|
|
397
|
+
// no write-to-disk path and no review attachment requested.
|
|
398
|
+
const needsRender = !appActive || !!args.output || args.review === true;
|
|
399
|
+
if (needsRender && (format === 'png' || args.review === true)) {
|
|
314
400
|
const result = await resolveFonts({ headless: true });
|
|
315
401
|
inputs.fonts = { sans: result.sans, mono: result.mono };
|
|
316
402
|
}
|
|
317
|
-
const
|
|
318
|
-
|
|
403
|
+
const bytes = needsRender
|
|
404
|
+
? await exportDocument(source, format, inputs, host)
|
|
405
|
+
: new Uint8Array(0);
|
|
406
|
+
const shareUrl = args.share
|
|
407
|
+
? (buildShareLink({ source, share: true }) ?? undefined)
|
|
408
|
+
: undefined;
|
|
409
|
+
const insights = await collectMcpLayoutInsights({
|
|
410
|
+
source,
|
|
411
|
+
filePath,
|
|
412
|
+
today,
|
|
413
|
+
theme: args.theme ?? 'light',
|
|
414
|
+
width: args.width,
|
|
415
|
+
locale: 'en-US',
|
|
416
|
+
readFile: host.readSource,
|
|
417
|
+
doc: blocked.doc,
|
|
418
|
+
});
|
|
419
|
+
const previewPayload = {
|
|
420
|
+
source,
|
|
421
|
+
theme: args.theme,
|
|
422
|
+
now: args.now,
|
|
423
|
+
width: args.width,
|
|
424
|
+
locale: 'en-US',
|
|
425
|
+
};
|
|
426
|
+
const reviewBlocks = args.review === true
|
|
427
|
+
? await buildReviewContentBlocks(source, format, bytes, inputs, host)
|
|
428
|
+
: [];
|
|
429
|
+
const insightHintBlocks = insights.length > 0 ? [{ type: 'text', text: LAYOUT_INSIGHT_HINT }] : [];
|
|
430
|
+
const insightsField = insights.length > 0 ? { insights } : {};
|
|
319
431
|
if (args.output) {
|
|
320
432
|
const outAbs = resolveAndGuard(args.output, allowedRoot);
|
|
321
433
|
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
322
434
|
await fs.writeFile(outAbs, bytes);
|
|
435
|
+
const structured = {
|
|
436
|
+
format,
|
|
437
|
+
path: outAbs,
|
|
438
|
+
bytes: bytes.byteLength,
|
|
439
|
+
shareUrl,
|
|
440
|
+
...insightsField,
|
|
441
|
+
};
|
|
323
442
|
return {
|
|
324
443
|
content: [
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
444
|
+
...(appActive ? [leanPreviewBlock(previewPayload)] : []),
|
|
445
|
+
{ type: 'text', text: JSON.stringify(structured, null, 2) },
|
|
446
|
+
...insightHintBlocks,
|
|
447
|
+
...reviewBlocks,
|
|
448
|
+
],
|
|
449
|
+
structuredContent: structured,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (appActive) {
|
|
453
|
+
const structured = {
|
|
454
|
+
format,
|
|
455
|
+
shareUrl,
|
|
456
|
+
...insightsField,
|
|
457
|
+
};
|
|
458
|
+
return {
|
|
459
|
+
content: [
|
|
460
|
+
leanPreviewBlock(previewPayload),
|
|
461
|
+
...insightHintBlocks,
|
|
462
|
+
...reviewBlocks,
|
|
329
463
|
],
|
|
464
|
+
structuredContent: structured,
|
|
330
465
|
};
|
|
331
466
|
}
|
|
332
467
|
if (format === 'png') {
|
|
468
|
+
const structured = { format, bytes: bytes.byteLength, shareUrl, ...insightsField };
|
|
333
469
|
return {
|
|
334
470
|
content: [
|
|
335
471
|
{
|
|
@@ -337,17 +473,21 @@ export function createMcpServer(opts = {}) {
|
|
|
337
473
|
data: Buffer.from(bytes).toString('base64'),
|
|
338
474
|
mimeType: 'image/png',
|
|
339
475
|
},
|
|
476
|
+
...insightHintBlocks,
|
|
477
|
+
...reviewBlocks,
|
|
340
478
|
],
|
|
479
|
+
structuredContent: structured,
|
|
341
480
|
};
|
|
342
481
|
}
|
|
482
|
+
const svgText = new TextDecoder('utf-8').decode(bytes);
|
|
483
|
+
const structured = { format, shareUrl, ...insightsField };
|
|
343
484
|
return {
|
|
344
485
|
content: [
|
|
345
|
-
{
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
mimeType: 'image/svg+xml',
|
|
349
|
-
},
|
|
486
|
+
{ type: 'text', text: svgText, mimeType: 'image/svg+xml' },
|
|
487
|
+
...insightHintBlocks,
|
|
488
|
+
...reviewBlocks,
|
|
350
489
|
],
|
|
490
|
+
structuredContent: structured,
|
|
351
491
|
};
|
|
352
492
|
});
|
|
353
493
|
// ---- export -------------------------------------------------------------
|
|
@@ -356,14 +496,22 @@ export function createMcpServer(opts = {}) {
|
|
|
356
496
|
description: 'Export a .nowline roadmap to any of the eight canonical formats. Byte-identical to `nowline -f <format>` for the same source and inputs.',
|
|
357
497
|
inputSchema: z.object({
|
|
358
498
|
source: z.string().optional().describe('Inline .nowline source text.'),
|
|
359
|
-
path: z
|
|
499
|
+
path: z
|
|
500
|
+
.string()
|
|
501
|
+
.optional()
|
|
502
|
+
.describe('Real local filesystem path to the .nowline file (e.g. /Users/name/Desktop/foo.nowline). ' +
|
|
503
|
+
'Never pass a virtual or sandbox path such as /mnt/user-data/… — ' +
|
|
504
|
+
'those do not exist on the host filesystem. Pass `source` instead.'),
|
|
360
505
|
format: z
|
|
361
506
|
.enum(EXPORT_FORMATS)
|
|
362
507
|
.describe('Export format: pdf, html, mermaid, xlsx, msproj, or png.'),
|
|
363
508
|
output: z
|
|
364
509
|
.string()
|
|
365
510
|
.optional()
|
|
366
|
-
.describe('
|
|
511
|
+
.describe('Real local filesystem path to write the output (e.g. /Users/name/Desktop/roadmap.pdf). ' +
|
|
512
|
+
'Required for binary formats (pdf, xlsx, msproj, png). ' +
|
|
513
|
+
'Never pass a virtual or sandbox path such as /mnt/user-data/… — ' +
|
|
514
|
+
'those do not exist on the host filesystem.'),
|
|
367
515
|
now: z.string().optional().describe('Now-line date as YYYY-MM-DD (UTC).'),
|
|
368
516
|
theme: z.enum(['light', 'dark', 'grayscale']).optional(),
|
|
369
517
|
scale: z.number().optional().describe('PNG scale factor.'),
|
|
@@ -377,9 +525,18 @@ export function createMcpServer(opts = {}) {
|
|
|
377
525
|
.string()
|
|
378
526
|
.optional()
|
|
379
527
|
.describe('MS Project start date override (YYYY-MM-DD).'),
|
|
528
|
+
share: z
|
|
529
|
+
.boolean()
|
|
530
|
+
.optional()
|
|
531
|
+
.describe('When true, include a shareUrl pointing to https://free.nowline.io/open.'),
|
|
380
532
|
}),
|
|
533
|
+
outputSchema: ExportOutputSchema,
|
|
534
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
381
535
|
}, async (args) => {
|
|
382
536
|
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
537
|
+
const blocked = await diagnosticsErrorBlock(source, filePath);
|
|
538
|
+
if (!blocked.ok)
|
|
539
|
+
return blocked.response;
|
|
383
540
|
const format = args.format;
|
|
384
541
|
const today = args.now ? new Date(`${args.now}T00:00:00Z`) : todayUtc();
|
|
385
542
|
const inputs = {
|
|
@@ -399,19 +556,19 @@ export function createMcpServer(opts = {}) {
|
|
|
399
556
|
}
|
|
400
557
|
const host = createNodeHostEnv(filePath);
|
|
401
558
|
const bytes = await exportDocument(source, format, inputs, host);
|
|
559
|
+
const shareUrl = args.share
|
|
560
|
+
? (buildShareLink({ source, share: true }) ?? undefined)
|
|
561
|
+
: undefined;
|
|
402
562
|
const BINARY_FORMATS = new Set(['png', 'pdf', 'xlsx']);
|
|
403
563
|
const isBinary = BINARY_FORMATS.has(format);
|
|
404
564
|
if (args.output) {
|
|
405
565
|
const outAbs = resolveAndGuard(args.output, allowedRoot);
|
|
406
566
|
await fs.mkdir(path.dirname(outAbs), { recursive: true });
|
|
407
567
|
await fs.writeFile(outAbs, bytes);
|
|
568
|
+
const structured = { format, path: outAbs, bytes: bytes.byteLength, shareUrl };
|
|
408
569
|
return {
|
|
409
|
-
content: [
|
|
410
|
-
|
|
411
|
-
type: 'text',
|
|
412
|
-
text: JSON.stringify({ path: outAbs, bytes: bytes.byteLength }),
|
|
413
|
-
},
|
|
414
|
-
],
|
|
570
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
571
|
+
structuredContent: structured,
|
|
415
572
|
};
|
|
416
573
|
}
|
|
417
574
|
if (isBinary) {
|
|
@@ -420,6 +577,7 @@ export function createMcpServer(opts = {}) {
|
|
|
420
577
|
pdf: 'application/pdf',
|
|
421
578
|
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
422
579
|
};
|
|
580
|
+
const structured = { format, bytes: bytes.byteLength, shareUrl };
|
|
423
581
|
return {
|
|
424
582
|
content: [
|
|
425
583
|
{
|
|
@@ -428,20 +586,272 @@ export function createMcpServer(opts = {}) {
|
|
|
428
586
|
mimeType: mimeMap[format] ?? 'application/octet-stream',
|
|
429
587
|
},
|
|
430
588
|
],
|
|
589
|
+
structuredContent: structured,
|
|
431
590
|
};
|
|
432
591
|
}
|
|
592
|
+
const text = new TextDecoder('utf-8').decode(bytes);
|
|
593
|
+
const structured = { format, shareUrl };
|
|
594
|
+
return {
|
|
595
|
+
content: [{ type: 'text', text }],
|
|
596
|
+
structuredContent: structured,
|
|
597
|
+
};
|
|
598
|
+
});
|
|
599
|
+
// ---- convert ------------------------------------------------------------
|
|
600
|
+
server.registerTool('convert', {
|
|
601
|
+
description: '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.',
|
|
602
|
+
inputSchema: z.object({
|
|
603
|
+
source: z.string().optional().describe('Inline source text to convert.'),
|
|
604
|
+
path: z
|
|
605
|
+
.string()
|
|
606
|
+
.optional()
|
|
607
|
+
.describe('Real local filesystem path to the source file. ' +
|
|
608
|
+
'Never pass a virtual or sandbox path such as /mnt/user-data/… — ' +
|
|
609
|
+
'pass `source` instead.'),
|
|
610
|
+
to: z
|
|
611
|
+
.enum(['json', 'nowline'])
|
|
612
|
+
.describe('"json" — serialize .nowline text to JSON AST. "nowline" — pretty-print a JSON AST back to .nowline source.'),
|
|
613
|
+
}),
|
|
614
|
+
outputSchema: ConvertOutputSchema,
|
|
615
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
616
|
+
}, async (args) => {
|
|
617
|
+
if (args.to === 'json') {
|
|
618
|
+
const { source, filePath } = await sourceAndPath(args, allowedRoot);
|
|
619
|
+
const host = createNodeHostEnv(filePath);
|
|
620
|
+
const jsonBytes = await exportDocument(source, 'json', {
|
|
621
|
+
sourcePath: filePath,
|
|
622
|
+
today: todayUtc(),
|
|
623
|
+
locale: 'en-US',
|
|
624
|
+
theme: 'light',
|
|
625
|
+
}, host);
|
|
626
|
+
const result = new TextDecoder('utf-8').decode(jsonBytes);
|
|
627
|
+
const structured = { to: 'json', result };
|
|
628
|
+
return {
|
|
629
|
+
content: [{ type: 'text', text: result }],
|
|
630
|
+
structuredContent: structured,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
// to: 'nowline' — input is a JSON AST string
|
|
634
|
+
const jsonSource = args.source ??
|
|
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.');
|
|
640
|
+
}
|
|
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
|
+
});
|
|
649
|
+
// ---- capabilities -------------------------------------------------------
|
|
650
|
+
server.registerTool('capabilities', {
|
|
651
|
+
description: 'Return all supported themes, icons, locales, export formats, and template names in a single response.',
|
|
652
|
+
inputSchema: z.object({}),
|
|
653
|
+
outputSchema: CapabilitiesOutputSchema,
|
|
654
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
655
|
+
}, async () => {
|
|
656
|
+
const structured = {
|
|
657
|
+
themes: [...CAPABILITIES.themes],
|
|
658
|
+
icons: [...CAPABILITIES.icons],
|
|
659
|
+
locales: [...CAPABILITIES.locales],
|
|
660
|
+
formats: [...CAPABILITIES.formats],
|
|
661
|
+
templates: [...CAPABILITIES.templates],
|
|
662
|
+
};
|
|
663
|
+
return {
|
|
664
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
665
|
+
structuredContent: structured,
|
|
666
|
+
};
|
|
667
|
+
});
|
|
668
|
+
// ---- list-themes --------------------------------------------------------
|
|
669
|
+
server.registerTool('list-themes', {
|
|
670
|
+
description: 'List supported color themes: light, dark, grayscale.',
|
|
671
|
+
inputSchema: z.object({}),
|
|
672
|
+
outputSchema: ListItemsOutputSchema,
|
|
673
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
674
|
+
}, async () => {
|
|
675
|
+
const structured = { items: [...CAPABILITIES.themes] };
|
|
676
|
+
return {
|
|
677
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
678
|
+
structuredContent: structured,
|
|
679
|
+
};
|
|
680
|
+
});
|
|
681
|
+
// ---- list-icons ---------------------------------------------------------
|
|
682
|
+
server.registerTool('list-icons', {
|
|
683
|
+
description: 'List built-in capacity-icon names usable in the `capacity-icon:` style property.',
|
|
684
|
+
inputSchema: z.object({}),
|
|
685
|
+
outputSchema: ListItemsOutputSchema,
|
|
686
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
687
|
+
}, async () => {
|
|
688
|
+
const structured = { items: [...CAPABILITIES.icons] };
|
|
689
|
+
return {
|
|
690
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
691
|
+
structuredContent: structured,
|
|
692
|
+
};
|
|
693
|
+
});
|
|
694
|
+
// ---- list-locales -------------------------------------------------------
|
|
695
|
+
server.registerTool('list-locales', {
|
|
696
|
+
description: 'List supported BCP-47 locale tags.',
|
|
697
|
+
inputSchema: z.object({}),
|
|
698
|
+
outputSchema: ListItemsOutputSchema,
|
|
699
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
700
|
+
}, async () => {
|
|
701
|
+
const structured = { items: [...CAPABILITIES.locales] };
|
|
702
|
+
return {
|
|
703
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
704
|
+
structuredContent: structured,
|
|
705
|
+
};
|
|
706
|
+
});
|
|
707
|
+
// ---- list-formats -------------------------------------------------------
|
|
708
|
+
server.registerTool('list-formats', {
|
|
709
|
+
description: 'List all supported export formats (svg, png, pdf, html, mermaid, xlsx, msproj, json).',
|
|
710
|
+
inputSchema: z.object({}),
|
|
711
|
+
outputSchema: ListItemsOutputSchema,
|
|
712
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
713
|
+
}, async () => {
|
|
714
|
+
const structured = { items: [...CAPABILITIES.formats] };
|
|
715
|
+
return {
|
|
716
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
717
|
+
structuredContent: structured,
|
|
718
|
+
};
|
|
719
|
+
});
|
|
720
|
+
// ---- list-templates -----------------------------------------------------
|
|
721
|
+
server.registerTool('list-templates', {
|
|
722
|
+
description: 'List built-in template names usable with `nowline --init --template`.',
|
|
723
|
+
inputSchema: z.object({}),
|
|
724
|
+
outputSchema: ListItemsOutputSchema,
|
|
725
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
726
|
+
}, async () => {
|
|
727
|
+
const structured = { items: [...CAPABILITIES.templates] };
|
|
728
|
+
return {
|
|
729
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
730
|
+
structuredContent: structured,
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
// ---- reference / examples / schema (discovery tools) --------------------
|
|
734
|
+
server.registerTool('reference', {
|
|
735
|
+
description: 'Return the Nowline DSL reference (condensed cheatsheet or full man page). Callable alternative to the nowline://reference resource.',
|
|
736
|
+
inputSchema: z.object({
|
|
737
|
+
format: z
|
|
738
|
+
.enum(['condensed', 'full'])
|
|
739
|
+
.optional()
|
|
740
|
+
.describe('Reference format. Defaults to condensed.'),
|
|
741
|
+
}),
|
|
742
|
+
outputSchema: ReferenceOutputSchema,
|
|
743
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
744
|
+
}, async (args) => {
|
|
745
|
+
const format = args.format ?? 'condensed';
|
|
746
|
+
const text = format === 'full' ? REFERENCE_MAN_PAGE : REFERENCE_CHEATSHEET;
|
|
747
|
+
const structured = { format, text };
|
|
748
|
+
return {
|
|
749
|
+
content: [{ type: 'text', text }],
|
|
750
|
+
structuredContent: structured,
|
|
751
|
+
};
|
|
752
|
+
});
|
|
753
|
+
server.registerTool('examples', {
|
|
754
|
+
description: 'Return canonical .nowline example sources. Callable alternative to the nowline://examples resource.',
|
|
755
|
+
inputSchema: z.object({
|
|
756
|
+
name: z
|
|
757
|
+
.string()
|
|
758
|
+
.optional()
|
|
759
|
+
.describe('Example name. Omit for the catalog plus minimal inline.'),
|
|
760
|
+
}),
|
|
761
|
+
outputSchema: ExamplesOutputSchema,
|
|
762
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
763
|
+
}, async (args) => {
|
|
764
|
+
const exampleNames = EXAMPLES.map((e) => exampleShortName(e.name));
|
|
765
|
+
if (args.name) {
|
|
766
|
+
const ex = findExample(args.name);
|
|
767
|
+
if (!ex) {
|
|
768
|
+
return {
|
|
769
|
+
content: [
|
|
770
|
+
{
|
|
771
|
+
type: 'text',
|
|
772
|
+
text: JSON.stringify({
|
|
773
|
+
error: `Unknown example "${args.name}".`,
|
|
774
|
+
names: exampleNames,
|
|
775
|
+
}),
|
|
776
|
+
},
|
|
777
|
+
],
|
|
778
|
+
isError: true,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const structured = { name: exampleShortName(ex.name), source: ex.content };
|
|
782
|
+
return {
|
|
783
|
+
content: [{ type: 'text', text: ex.content }],
|
|
784
|
+
structuredContent: structured,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
const minimal = findExample('minimal') ?? EXAMPLES[0];
|
|
788
|
+
const structured = {
|
|
789
|
+
names: exampleNames,
|
|
790
|
+
name: exampleShortName(minimal.name),
|
|
791
|
+
source: minimal.content,
|
|
792
|
+
};
|
|
433
793
|
return {
|
|
434
794
|
content: [
|
|
435
795
|
{
|
|
436
796
|
type: 'text',
|
|
437
|
-
text:
|
|
797
|
+
text: `# Examples\n\n${exampleNames.map((n) => `- ${n}`).join('\n')}\n\n## ${structured.name}\n\n${minimal.content}`,
|
|
438
798
|
},
|
|
439
799
|
],
|
|
800
|
+
structuredContent: structured,
|
|
801
|
+
};
|
|
802
|
+
});
|
|
803
|
+
server.registerTool('schema', {
|
|
804
|
+
description: 'Return the structured Nowline DSL key vocabulary (directive keys, entity types, item properties).',
|
|
805
|
+
inputSchema: z.object({}),
|
|
806
|
+
outputSchema: SchemaOutputSchema,
|
|
807
|
+
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
808
|
+
}, async () => {
|
|
809
|
+
const structured = {
|
|
810
|
+
directiveKeys: [...SCHEMA_VOCABULARY.directiveKeys],
|
|
811
|
+
entityTypes: [...SCHEMA_VOCABULARY.entityTypes],
|
|
812
|
+
itemPropertyKeys: [...SCHEMA_VOCABULARY.itemPropertyKeys],
|
|
813
|
+
};
|
|
814
|
+
return {
|
|
815
|
+
content: [{ type: 'text', text: JSON.stringify(structured, null, 2) }],
|
|
816
|
+
structuredContent: structured,
|
|
440
817
|
};
|
|
441
818
|
});
|
|
442
819
|
return server;
|
|
443
820
|
}
|
|
444
|
-
|
|
821
|
+
async function buildReviewContentBlocks(source, format, artifactBytes, inputs, host) {
|
|
822
|
+
const blocks = [
|
|
823
|
+
{
|
|
824
|
+
type: 'text',
|
|
825
|
+
text: 'Review this raster for layout issues (truncated labels, crowded lanes, ' +
|
|
826
|
+
'now-line position) before finalizing.',
|
|
827
|
+
},
|
|
828
|
+
];
|
|
829
|
+
const artifactWidth = inputs.width ?? DEFAULT_RENDER_WIDTH;
|
|
830
|
+
let inspectionBytes;
|
|
831
|
+
if (format === 'png' && artifactWidth <= REVIEW_MAX_WIDTH) {
|
|
832
|
+
inspectionBytes = artifactBytes;
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
const reviewInputs = {
|
|
836
|
+
...inputs,
|
|
837
|
+
width: Math.min(artifactWidth, REVIEW_MAX_WIDTH),
|
|
838
|
+
pngScale: 1,
|
|
839
|
+
};
|
|
840
|
+
if (!reviewInputs.fonts) {
|
|
841
|
+
const fonts = await resolveFonts({ headless: true });
|
|
842
|
+
reviewInputs.fonts = { sans: fonts.sans, mono: fonts.mono };
|
|
843
|
+
}
|
|
844
|
+
inspectionBytes = await exportDocument(source, 'png', reviewInputs, host);
|
|
845
|
+
}
|
|
846
|
+
if (format !== 'png' || inspectionBytes.byteLength !== artifactBytes.byteLength) {
|
|
847
|
+
blocks.push({
|
|
848
|
+
type: 'image',
|
|
849
|
+
data: Buffer.from(inspectionBytes).toString('base64'),
|
|
850
|
+
mimeType: 'image/png',
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
return blocks;
|
|
854
|
+
}
|
|
445
855
|
async function listNowlineFiles(dir, recursive) {
|
|
446
856
|
const results = [];
|
|
447
857
|
try {
|