@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.
Files changed (64) hide show
  1. package/dist/branding.d.ts +20 -0
  2. package/dist/branding.d.ts.map +1 -0
  3. package/dist/branding.js +27 -0
  4. package/dist/branding.js.map +1 -0
  5. package/dist/capabilities.d.ts +9 -0
  6. package/dist/capabilities.d.ts.map +1 -0
  7. package/dist/capabilities.js +17 -0
  8. package/dist/capabilities.js.map +1 -0
  9. package/dist/diagnostics.d.ts +59 -0
  10. package/dist/diagnostics.d.ts.map +1 -0
  11. package/dist/diagnostics.js +117 -0
  12. package/dist/diagnostics.js.map +1 -0
  13. package/dist/generated/resources.d.ts +2 -0
  14. package/dist/generated/resources.d.ts.map +1 -1
  15. package/dist/generated/resources.js +6 -1
  16. package/dist/generated/resources.js.map +1 -1
  17. package/dist/generated/ui-bundle.d.ts +5 -0
  18. package/dist/generated/ui-bundle.d.ts.map +1 -0
  19. package/dist/generated/ui-bundle.js +11 -0
  20. package/dist/generated/ui-bundle.js.map +1 -0
  21. package/dist/index.js +63 -12
  22. package/dist/index.js.map +1 -1
  23. package/dist/prompts.d.ts +3 -0
  24. package/dist/prompts.d.ts.map +1 -0
  25. package/dist/prompts.js +143 -0
  26. package/dist/prompts.js.map +1 -0
  27. package/dist/reference-cheatsheet.d.ts +2 -0
  28. package/dist/reference-cheatsheet.d.ts.map +1 -0
  29. package/dist/reference-cheatsheet.js +47 -0
  30. package/dist/reference-cheatsheet.js.map +1 -0
  31. package/dist/schema-vocab.d.ts +6 -0
  32. package/dist/schema-vocab.d.ts.map +1 -0
  33. package/dist/schema-vocab.js +55 -0
  34. package/dist/schema-vocab.js.map +1 -0
  35. package/dist/schemas.d.ts +148 -0
  36. package/dist/schemas.d.ts.map +1 -0
  37. package/dist/schemas.js +88 -0
  38. package/dist/schemas.js.map +1 -0
  39. package/dist/server.d.ts +2 -0
  40. package/dist/server.d.ts.map +1 -1
  41. package/dist/server.js +531 -121
  42. package/dist/server.js.map +1 -1
  43. package/dist/ui/entry.d.ts +2 -0
  44. package/dist/ui/entry.d.ts.map +1 -0
  45. package/dist/ui/entry.js +187 -0
  46. package/dist/ui/entry.js.map +1 -0
  47. package/dist/ui/payload.d.ts +30 -0
  48. package/dist/ui/payload.d.ts.map +1 -0
  49. package/dist/ui/payload.js +49 -0
  50. package/dist/ui/payload.js.map +1 -0
  51. package/package.json +15 -6
  52. package/src/branding.ts +26 -0
  53. package/src/capabilities.ts +25 -0
  54. package/src/diagnostics.ts +185 -0
  55. package/src/generated/resources.ts +7 -1
  56. package/src/generated/ui-bundle.ts +12 -0
  57. package/src/index.ts +75 -13
  58. package/src/prompts.ts +172 -0
  59. package/src/reference-cheatsheet.ts +47 -0
  60. package/src/schema-vocab.ts +55 -0
  61. package/src/schemas.ts +106 -0
  62. package/src/server.ts +725 -139
  63. package/src/ui/entry.ts +214 -0
  64. package/src/ui/payload.ts +63 -0
@@ -0,0 +1,214 @@
1
+ // Browser entry bundled into the MCP Apps in-chat live preview.
2
+ //
3
+ // The MCP host loads a pre-declared ui:// HTML resource and hydrates it via the
4
+ // ext-apps handshake. The widget mounts from the earliest signal available:
5
+ // 1. ontoolinput — the LLM's tool arguments, delivered before the server runs.
6
+ // This is the primary, fast path for the common inline-`source` case and
7
+ // mirrors the official ext-apps examples (e.g. mermaid-mcp-app), which paint
8
+ // from tool input rather than waiting on the result notification.
9
+ // 2. ontoolresult — the lean { kind: 'nowline.preview', source, … } payload the
10
+ // server returns. Authoritative: it carries the resolved `source` even when
11
+ // the caller passed `path:` instead of `source:`, so it reconciles the input
12
+ // render and covers hosts that never deliver ontoolinput.
13
+ // A #nl-preview-data JSON <script> fallback remains for the /widget-preview dev
14
+ // shim and non-handshake degradation.
15
+ //
16
+ // Mirrors packages/vscode-extension/src/preview/webview/entry.ts; this entry
17
+ // owns the render (no host editor feeding it SVG), so theme / now / show-links
18
+ // changes re-render in-place.
19
+
20
+ // Runs in a browser-like iframe, not Node. Pull in the DOM lib only here
21
+ // (the rest of @nowline/mcp is Node) — mirrors the VS Code webview entry.
22
+ /// <reference lib="dom" />
23
+
24
+ // Injected by bundle-ui.mjs at build time from package.json; not a runtime import.
25
+ declare const __MCP_VERSION__: string;
26
+
27
+ import { App } from '@modelcontextprotocol/ext-apps';
28
+ import { mountLivePreview } from '@nowline/preview';
29
+ import type { NowOverride, ThemeOverride } from '@nowline/preview-shell';
30
+ import {
31
+ type PreviewPayload,
32
+ parsePreviewFromArguments,
33
+ parsePreviewFromContent,
34
+ } from './payload.js';
35
+
36
+ function readPayload(): PreviewPayload | undefined {
37
+ const el = document.getElementById('nl-preview-data');
38
+ if (!el?.textContent) return undefined;
39
+ try {
40
+ return JSON.parse(el.textContent) as PreviewPayload;
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }
45
+
46
+ /** Coerce a raw theme token from the payload to the shell's ThemeOverride. */
47
+ function toThemeOverride(theme: string | undefined): ThemeOverride {
48
+ switch (theme) {
49
+ case 'light':
50
+ case 'dark':
51
+ case 'grayscale':
52
+ case 'greyscale':
53
+ case 'auto':
54
+ return theme;
55
+ default:
56
+ return 'auto';
57
+ }
58
+ }
59
+
60
+ let mounted = false;
61
+ let mountedSource: string | undefined;
62
+
63
+ function mountFromPayload(payload: PreviewPayload): void {
64
+ const root = document.getElementById('nl-preview-root');
65
+ if (!root || typeof payload.source !== 'string') {
66
+ return;
67
+ }
68
+
69
+ // Idempotent across the ontoolinput → ontoolresult sequence: the result
70
+ // notification normally repeats the same `source` the input already mounted,
71
+ // so skip it. A genuinely different `source` (e.g. path resolution, or a
72
+ // re-invocation on the same iframe) tears down the prior mount first.
73
+ if (mounted) {
74
+ if (payload.source === mountedSource) return;
75
+ (root as HTMLElement).replaceChildren();
76
+ }
77
+
78
+ mountLivePreview(root as HTMLElement, {
79
+ source: payload.source,
80
+ initialView: {
81
+ theme: toThemeOverride(payload.theme),
82
+ now: (payload.now ?? 'today') as NowOverride,
83
+ showLinks: payload.showLinks !== false,
84
+ },
85
+ renderOptions: {
86
+ width: payload.width,
87
+ locale: payload.locale,
88
+ },
89
+ themeControl: 'show',
90
+ exportControls: 'hide',
91
+ locale: payload.locale,
92
+ // Inline chat widget: fill the width and take the diagram's natural
93
+ // height (reportHeight sizes the iframe to match), rather than fitPage
94
+ // which centers a width-constrained diagram and leaves empty space.
95
+ initialFit: payload.initialFit ?? 'fitWidth',
96
+ showMinimap: payload.showMinimap,
97
+ });
98
+ mounted = true;
99
+ mountedSource = payload.source;
100
+ }
101
+
102
+ // Sizing for a size-to-content host (Claude Desktop), which sizes the iframe
103
+ // from our ui/notifications/size-changed. The shell is a fill-the-container
104
+ // layout (html / body / #nl-preview-root are height:100%) with no intrinsic
105
+ // height, so the ext-apps SDK's default autoResize — which measures
106
+ // documentElement at `max-content` — collapses to 0 and the host shrinks the
107
+ // iframe to nothing (blank, no console error). We disable autoResize and report
108
+ // the *diagram's* height instead: the rendered SVG shown at fit-width, clamped
109
+ // to the host's available height. A short roadmap stays compact (no empty band
110
+ // pushing it below the fold); a tall one is capped and scrolls internally. User
111
+ // zoom does not resize the iframe (the report is viewBox-based, scale-free).
112
+ // VS Code / embed own their panel height and never mount this entry.
113
+ const MIN_HEIGHT_PX = 160;
114
+ const DEFAULT_MAX_HEIGHT_PX = 640;
115
+ const PREPAINT_HEIGHT_PX = 360;
116
+
117
+ function hostMaxHeight(app: App): number {
118
+ const dims = app.getHostContext()?.containerDimensions as
119
+ | { height?: number; maxHeight?: number }
120
+ | undefined;
121
+ return Math.round(dims?.maxHeight ?? dims?.height ?? DEFAULT_MAX_HEIGHT_PX);
122
+ }
123
+
124
+ /** viewBox aspect of the rendered diagram, or null before the first paint. */
125
+ function diagramNaturalSize(): { w: number; h: number } | null {
126
+ const svg = document.querySelector('#nl-preview-root svg') as SVGSVGElement | null;
127
+ const vb = svg?.viewBox?.baseVal;
128
+ return vb && vb.width > 0 && vb.height > 0 ? { w: vb.width, h: vb.height } : null;
129
+ }
130
+
131
+ let lastReportedHeight = -1;
132
+
133
+ function desiredHeight(app: App): number {
134
+ const cap = Math.max(hostMaxHeight(app), MIN_HEIGHT_PX);
135
+ const nat = diagramNaturalSize();
136
+ const width = document.documentElement.clientWidth || 0;
137
+ if (!nat || width <= 0) {
138
+ // Pre-paint, or a diagnostics-only state: a modest non-zero height.
139
+ return Math.min(Math.max(PREPAINT_HEIGHT_PX, MIN_HEIGHT_PX), cap);
140
+ }
141
+ // Fit-width display height = naturalHeight × (width / naturalWidth).
142
+ const fitHeight = Math.ceil(nat.h * (width / nat.w));
143
+ return Math.min(Math.max(fitHeight, MIN_HEIGHT_PX), cap);
144
+ }
145
+
146
+ function reportHeight(app: App): void {
147
+ const height = desiredHeight(app);
148
+ if (height === lastReportedHeight) return;
149
+ lastReportedHeight = height;
150
+ // Concrete base for the height:100% chain (so the fill layout paints) and the
151
+ // height the host should give the iframe.
152
+ document.documentElement.style.height = `${height}px`;
153
+ void app.sendSizeChanged({ height });
154
+ }
155
+
156
+ function watchSize(app: App): void {
157
+ const root = document.getElementById('nl-preview-root');
158
+ if (!root) return;
159
+ let scheduled = false;
160
+ const trigger = (): void => {
161
+ if (scheduled) return;
162
+ scheduled = true;
163
+ requestAnimationFrame(() => {
164
+ scheduled = false;
165
+ reportHeight(app);
166
+ });
167
+ };
168
+ // Re-measure when the diagram (re)renders into the canvas…
169
+ if (typeof MutationObserver !== 'undefined') {
170
+ new MutationObserver(trigger).observe(root, { childList: true, subtree: true });
171
+ }
172
+ // …and when the host changes our width.
173
+ if (typeof ResizeObserver !== 'undefined') {
174
+ new ResizeObserver(trigger).observe(root);
175
+ }
176
+ }
177
+
178
+ async function bootstrap(): Promise<void> {
179
+ // autoResize:false — see the sizing block above.
180
+ const app = new App(
181
+ { name: 'NowlinePreview', version: __MCP_VERSION__ },
182
+ {},
183
+ { autoResize: false },
184
+ );
185
+
186
+ // Primary path: paint from the LLM's tool arguments the moment they arrive,
187
+ // before the server finishes rendering. Mirrors the official ext-apps examples.
188
+ app.ontoolinput = ({ arguments: args }) => {
189
+ const payload = parsePreviewFromArguments(args as Record<string, unknown> | undefined);
190
+ if (payload) mountFromPayload(payload);
191
+ };
192
+
193
+ // Authoritative path: the server-resolved lean preview payload. Reconciles the
194
+ // input render and covers the `path:`-only case and hosts without ontoolinput.
195
+ app.ontoolresult = ({ content, isError }) => {
196
+ if (isError) return;
197
+ const payload = parsePreviewFromContent(content);
198
+ if (payload) mountFromPayload(payload);
199
+ };
200
+
201
+ // Re-report when the host changes our width / available height.
202
+ app.onhostcontextchanged = () => reportHeight(app);
203
+
204
+ await app.connect();
205
+ watchSize(app);
206
+ reportHeight(app);
207
+
208
+ if (!mounted) {
209
+ const fallback = readPayload();
210
+ if (fallback) mountFromPayload(fallback);
211
+ }
212
+ }
213
+
214
+ void bootstrap();
@@ -0,0 +1,63 @@
1
+ // Pure helpers shared by the MCP Apps preview entry (src/ui/entry.ts).
2
+ //
3
+ // Kept DOM-free and side-effect-free so they are unit-testable in Node: entry.ts
4
+ // runs bootstrap() on import and touches the iframe DOM, so its logic can't be
5
+ // imported directly under Vitest. The widget derives its render inputs from two
6
+ // ext-apps signals — tool arguments (ontoolinput) and the tool result
7
+ // (ontoolresult) — and both mappings live here.
8
+
9
+ /** Server-injected or per-call render inputs for the live preview. */
10
+ export interface PreviewPayload {
11
+ kind?: string;
12
+ source: string;
13
+ theme?: string;
14
+ now?: string;
15
+ width?: number;
16
+ locale?: string;
17
+ showLinks?: boolean;
18
+ showMinimap?: boolean;
19
+ initialFit?: 'fitPage' | 'fitWidth' | 'actual';
20
+ }
21
+
22
+ /**
23
+ * Extract the lean `{ kind: 'nowline.preview', source, … }` payload from a tool
24
+ * result's content blocks (the `ontoolresult` notification). This is the
25
+ * authoritative path: the server resolves `path:` → `source` before emitting it.
26
+ */
27
+ export function parsePreviewFromContent(
28
+ content: Array<{ type: string; text?: string }> | undefined,
29
+ ): PreviewPayload | undefined {
30
+ if (!content) return undefined;
31
+ for (const block of content) {
32
+ if (block.type !== 'text' || !block.text) continue;
33
+ try {
34
+ const parsed = JSON.parse(block.text) as PreviewPayload;
35
+ if (parsed.kind === 'nowline.preview' && typeof parsed.source === 'string') {
36
+ return parsed;
37
+ }
38
+ } catch {
39
+ /* not JSON — skip */
40
+ }
41
+ }
42
+ return undefined;
43
+ }
44
+
45
+ /**
46
+ * Build a preview payload from the LLM's raw `render` tool arguments
47
+ * (the `ontoolinput` notification). Only viable for the inline-`source` case —
48
+ * when the caller passed `path:` instead, the file can't be read in the iframe,
49
+ * so we return undefined and wait for `ontoolresult` (which carries the
50
+ * server-resolved `source`). Locale mirrors the server default (`en-US`).
51
+ */
52
+ export function parsePreviewFromArguments(
53
+ args: Record<string, unknown> | undefined,
54
+ ): PreviewPayload | undefined {
55
+ if (!args || typeof args.source !== 'string') return undefined;
56
+ return {
57
+ source: args.source,
58
+ theme: typeof args.theme === 'string' ? args.theme : undefined,
59
+ now: typeof args.now === 'string' ? args.now : undefined,
60
+ width: typeof args.width === 'number' ? args.width : undefined,
61
+ locale: 'en-US',
62
+ };
63
+ }