sceneview-mcp 3.4.8 → 3.4.10

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.
@@ -0,0 +1,392 @@
1
+ // ─── 3D Artifact Generator ───────────────────────────────────────────────────
2
+ //
3
+ // Generates complete, self-contained HTML artifacts with interactive 3D content
4
+ // that Claude can render directly in conversations via artifacts.
5
+ //
6
+ // Uses Filament.js (Google's PBR renderer, WASM) for real 3D rendering —
7
+ // the same engine as SceneView Android.
8
+ //
9
+ // Types:
10
+ // - model-viewer: interactive 3D model viewer with orbit controls (Filament.js)
11
+ // - chart-3d: 3D bar/pie charts for data visualization (CSS 3D)
12
+ // - scene: multi-model 3D scene with lighting and environment (Filament.js)
13
+ // - product-360: product turntable with hotspot annotations (Filament.js)
14
+ // ─── Constants ───────────────────────────────────────────────────────────────
15
+ const FILAMENT_CDN = "https://cdn.jsdelivr.net/npm/filament@1.52.3/filament.js";
16
+ const DEFAULT_MODEL = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/DamagedHelmet/glTF-Binary/DamagedHelmet.glb";
17
+ const DEFAULT_COLORS = [
18
+ "#4285F4", "#EA4335", "#FBBC04", "#34A853", "#FF6D01",
19
+ "#46BDC6", "#7BAAF7", "#F07B72", "#FCD04F", "#57BB8A",
20
+ ];
21
+ const BRANDING = `<div style="position:absolute;bottom:8px;right:12px;font-size:11px;color:#888;font-family:system-ui,sans-serif;pointer-events:none">Powered by SceneView</div>`;
22
+ // ─── Shared HTML skeleton ────────────────────────────────────────────────────
23
+ function htmlShell(title, body, extraHead = "") {
24
+ return `<!DOCTYPE html>
25
+ <html lang="en">
26
+ <head>
27
+ <meta charset="utf-8">
28
+ <meta name="viewport" content="width=device-width,initial-scale=1">
29
+ <title>${escapeHtml(title)}</title>
30
+ <style>
31
+ *{margin:0;padding:0;box-sizing:border-box}
32
+ html,body{width:100%;height:100%;overflow:hidden;background:#0d1117;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
33
+ </style>
34
+ ${extraHead}
35
+ </head>
36
+ <body>
37
+ ${body}
38
+ </body>
39
+ </html>`;
40
+ }
41
+ function escapeHtml(s) {
42
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
43
+ }
44
+ // ─── Validation ──────────────────────────────────────────────────────────────
45
+ export function validateArtifactInput(input) {
46
+ const validTypes = ["model-viewer", "chart-3d", "scene", "product-360"];
47
+ if (!validTypes.includes(input.type)) {
48
+ return `Invalid type "${input.type}". Must be one of: ${validTypes.join(", ")}`;
49
+ }
50
+ if (input.type === "chart-3d") {
51
+ if (!input.data || !Array.isArray(input.data) || input.data.length === 0) {
52
+ return 'Type "chart-3d" requires a non-empty `data` array with {label, value} objects.';
53
+ }
54
+ for (const d of input.data) {
55
+ if (typeof d.label !== "string" || typeof d.value !== "number") {
56
+ return "Each data item must have a string `label` and numeric `value`.";
57
+ }
58
+ }
59
+ }
60
+ if (input.modelUrl) {
61
+ if (!input.modelUrl.startsWith("https://") && !input.modelUrl.startsWith("http://")) {
62
+ return "modelUrl must be an HTTP(S) URL.";
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ // ─── Generators ──────────────────────────────────────────────────────────────
68
+ export function generateArtifact(input) {
69
+ switch (input.type) {
70
+ case "model-viewer":
71
+ return generateModelViewer(input);
72
+ case "chart-3d":
73
+ return generateChart3D(input);
74
+ case "scene":
75
+ return generateScene(input);
76
+ case "product-360":
77
+ return generateProduct360(input);
78
+ }
79
+ }
80
+ // ─── Filament.js renderer core ──────────────────────────────────────────────
81
+ //
82
+ // Shared inline script that sets up the Filament engine, scene, camera,
83
+ // lights, orbit controls, and render loop. Used by model-viewer, scene,
84
+ // and product-360 artifact types.
85
+ function filamentRendererScript(options) {
86
+ const { modelUrl, bgColor, autoRotate, orbitRadius = 3.5, orbitHeight = 0.8, sunIntensity = 110000, fillIntensity = 30000, } = options;
87
+ return `<script src="${FILAMENT_CDN}"><\/script>
88
+ <script>
89
+ Filament.init(['${modelUrl}'], function() {
90
+ try {
91
+ var canvas = document.getElementById('viewer');
92
+ canvas.width = canvas.clientWidth * devicePixelRatio;
93
+ canvas.height = canvas.clientHeight * devicePixelRatio;
94
+
95
+ var engine = Filament.Engine.create(canvas);
96
+ var scene = engine.createScene();
97
+ var renderer = engine.createRenderer();
98
+ var cam = engine.createCamera(Filament.EntityManager.get().create());
99
+ var view = engine.createView();
100
+ var sc = engine.createSwapChain();
101
+
102
+ view.setCamera(cam);
103
+ view.setScene(scene);
104
+ view.setViewport([0, 0, canvas.width, canvas.height]);
105
+ renderer.setClearOptions({ clearColor: [${bgColor.join(",")}], clear: true });
106
+ cam.setProjectionFov(45, canvas.width / canvas.height, 0.1, 100, Filament.Camera$Fov.VERTICAL);
107
+
108
+ // Sun light (warm key light)
109
+ var sun = Filament.EntityManager.get().create();
110
+ Filament.LightManager.Builder(Filament.LightManager$Type.SUN)
111
+ .color([0.98, 0.92, 0.89]).intensity(${sunIntensity}).direction([0.6, -1, -0.8])
112
+ .sunAngularRadius(1.9).sunHaloSize(10).sunHaloFalloff(80)
113
+ .build(engine, sun);
114
+ scene.addEntity(sun);
115
+
116
+ // Fill light (cool)
117
+ var fill = Filament.EntityManager.get().create();
118
+ Filament.LightManager.Builder(Filament.LightManager$Type.DIRECTIONAL)
119
+ .color([0.6, 0.65, 0.8]).intensity(${fillIntensity}).direction([-0.5, 0.5, 1])
120
+ .build(engine, fill);
121
+ scene.addEntity(fill);
122
+
123
+ // Load model
124
+ var loader = engine.createAssetLoader();
125
+ var asset = loader.createAsset(Filament.assets['${modelUrl}']);
126
+ if (asset) {
127
+ asset.loadResources();
128
+ scene.addEntity(asset.getRoot());
129
+ scene.addEntities(asset.getRenderableEntities());
130
+ }
131
+
132
+ // Orbit controls
133
+ var angle = 0, orbitR = ${orbitRadius}, orbitH = ${orbitHeight};
134
+ var dragging = false, lastX = 0, lastY = 0, autoRotate = ${autoRotate};
135
+
136
+ canvas.addEventListener('mousedown', function(e) { dragging = true; lastX = e.clientX; lastY = e.clientY; autoRotate = false; });
137
+ canvas.addEventListener('mousemove', function(e) { if (!dragging) return; angle += (e.clientX - lastX) * 0.005; orbitH += (e.clientY - lastY) * 0.01; lastX = e.clientX; lastY = e.clientY; });
138
+ canvas.addEventListener('mouseup', function() { dragging = false; });
139
+ canvas.addEventListener('mouseleave', function() { dragging = false; });
140
+ canvas.addEventListener('wheel', function(e) { e.preventDefault(); orbitR *= (1 + e.deltaY * 0.001); orbitR = Math.max(0.5, Math.min(20, orbitR)); }, { passive: false });
141
+ canvas.addEventListener('touchstart', function(e) { if (e.touches.length === 1) { dragging = true; lastX = e.touches[0].clientX; lastY = e.touches[0].clientY; autoRotate = false; } });
142
+ canvas.addEventListener('touchmove', function(e) { if (!dragging) return; e.preventDefault(); angle += (e.touches[0].clientX - lastX) * 0.005; orbitH += (e.touches[0].clientY - lastY) * 0.01; lastX = e.touches[0].clientX; lastY = e.touches[0].clientY; }, { passive: false });
143
+ canvas.addEventListener('touchend', function() { dragging = false; });
144
+
145
+ // Handle resize
146
+ new ResizeObserver(function() {
147
+ canvas.width = canvas.clientWidth * devicePixelRatio;
148
+ canvas.height = canvas.clientHeight * devicePixelRatio;
149
+ view.setViewport([0, 0, canvas.width, canvas.height]);
150
+ cam.setProjectionFov(45, canvas.width / canvas.height, 0.1, 100, Filament.Camera$Fov.VERTICAL);
151
+ }).observe(canvas);
152
+
153
+ // Render loop
154
+ function render() {
155
+ if (autoRotate) angle += 0.006;
156
+ cam.lookAt([Math.sin(angle) * orbitR, orbitH, Math.cos(angle) * orbitR], [0, 0, 0], [0, 1, 0]);
157
+ if (renderer.beginFrame(sc)) { renderer.render(sc, view); renderer.endFrame(); }
158
+ engine.execute();
159
+ requestAnimationFrame(render);
160
+ }
161
+ render();
162
+
163
+ // Update status
164
+ var statusEl = document.getElementById('status');
165
+ if (statusEl) { statusEl.textContent = 'Drag to rotate \\u00b7 Scroll to zoom'; statusEl.style.color = '#34a853'; }
166
+
167
+ } catch(e) {
168
+ var statusEl = document.getElementById('status');
169
+ if (statusEl) { statusEl.textContent = 'Error: ' + e.message; statusEl.style.color = '#ea4335'; }
170
+ console.error(e);
171
+ }
172
+ });
173
+ <\/script>`;
174
+ }
175
+ /** Convert hex color to Filament RGBA [0-1] range */
176
+ function hexToBgColor(hex) {
177
+ const rgb = hexToRgb(hex);
178
+ return [rgb.r / 255, rgb.g / 255, rgb.b / 255, 1.0];
179
+ }
180
+ // ── model-viewer ─────────────────────────────────────────────────────────────
181
+ function generateModelViewer(input) {
182
+ const model = input.modelUrl || DEFAULT_MODEL;
183
+ const title = input.title || "3D Model Viewer";
184
+ const opts = input.options || {};
185
+ const bg = opts.backgroundColor || "#0d1117";
186
+ const autoRotate = opts.autoRotate !== false;
187
+ const body = `
188
+ <style>
189
+ canvas{width:100%;height:100%;cursor:grab;display:block}
190
+ canvas:active{cursor:grabbing}
191
+ .title{position:absolute;top:16px;left:16px;font-size:18px;font-weight:600;color:#fff;text-shadow:0 2px 8px rgba(0,0,0,0.5);z-index:10}
192
+ #status{position:absolute;top:44px;left:16px;font-size:12px;color:#8ab4f8;z-index:10}
193
+ </style>
194
+ <div class="title">${escapeHtml(title)}</div>
195
+ <div id="status">Loading Filament.js WASM...</div>
196
+ <canvas id="viewer"></canvas>
197
+ ${BRANDING}
198
+ ${filamentRendererScript({ modelUrl: model, bgColor: hexToBgColor(bg), autoRotate })}`;
199
+ return {
200
+ html: htmlShell(title, body),
201
+ title,
202
+ type: "model-viewer",
203
+ };
204
+ }
205
+ // ── chart-3d ─────────────────────────────────────────────────────────────────
206
+ function generateChart3D(input) {
207
+ const title = input.title || "3D Data Visualization";
208
+ const data = input.data;
209
+ const bg = input.options?.backgroundColor || "#0d1117";
210
+ const maxVal = Math.max(...data.map((d) => d.value));
211
+ const bars = data
212
+ .map((d, i) => {
213
+ const color = d.color || DEFAULT_COLORS[i % DEFAULT_COLORS.length];
214
+ const heightPct = maxVal > 0 ? (d.value / maxVal) * 100 : 0;
215
+ const height = Math.max(heightPct * 2.5, 8); // px scale, min 8px
216
+ return `
217
+ <div class="bar-group" style="--delay:${i * 0.08}s">
218
+ <div class="bar-wrapper">
219
+ <div class="bar-value">${formatNumber(d.value)}</div>
220
+ <div class="bar" style="height:${height}px;background:linear-gradient(180deg,${color},${darken(color, 0.3)});box-shadow:4px 4px 0 ${darken(color, 0.5)}, 0 0 20px ${color}40">
221
+ <div class="bar-face-top" style="background:${lighten(color, 0.15)}"></div>
222
+ <div class="bar-face-right" style="background:${darken(color, 0.2)}"></div>
223
+ </div>
224
+ </div>
225
+ <div class="bar-label">${escapeHtml(d.label)}</div>
226
+ </div>`;
227
+ })
228
+ .join("\n");
229
+ const body = `
230
+ <style>
231
+ body{background:${bg};display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;perspective:800px;overflow:hidden}
232
+ .chart-title{font-size:22px;font-weight:700;margin-bottom:24px;color:#fff;letter-spacing:-0.5px}
233
+ .chart-subtitle{font-size:13px;color:#888;margin-bottom:32px}
234
+ .chart-container{display:flex;align-items:flex-end;gap:clamp(8px,2vw,24px);padding:24px 32px;transform:rotateX(8deg) rotateY(-8deg);transform-style:preserve-3d;background:rgba(255,255,255,0.03);border-radius:16px;border:1px solid rgba(255,255,255,0.06);max-width:95vw;overflow-x:auto}
235
+ .bar-group{display:flex;flex-direction:column;align-items:center;animation:barIn 0.6s ease-out var(--delay) both}
236
+ .bar-wrapper{position:relative;display:flex;flex-direction:column;align-items:center}
237
+ .bar{width:clamp(28px,6vw,56px);border-radius:4px 4px 0 0;position:relative;transform-style:preserve-3d;transition:height 0.5s ease-out}
238
+ .bar-face-top{position:absolute;top:-6px;left:-2px;right:-2px;height:8px;border-radius:4px 4px 0 0;transform:rotateX(45deg);transform-origin:bottom}
239
+ .bar-face-right{position:absolute;top:0;right:-6px;bottom:0;width:8px;border-radius:0 4px 4px 0;transform:rotateY(45deg);transform-origin:left}
240
+ .bar-value{font-size:12px;font-weight:600;color:#fff;margin-bottom:6px;white-space:nowrap}
241
+ .bar-label{font-size:11px;color:#aaa;margin-top:8px;text-align:center;max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
242
+ @keyframes barIn{from{opacity:0;transform:scaleY(0) translateY(20px)}to{opacity:1;transform:scaleY(1) translateY(0)}}
243
+ .total-row{margin-top:24px;font-size:14px;color:#aaa}
244
+ .total-row strong{color:#fff;font-size:16px}
245
+ </style>
246
+ <div class="chart-title">${escapeHtml(title)}</div>
247
+ <div class="chart-subtitle">${data.length} data points</div>
248
+ <div class="chart-container">
249
+ ${bars}
250
+ </div>
251
+ <div class="total-row">Total: <strong>${formatNumber(data.reduce((s, d) => s + d.value, 0))}</strong></div>
252
+ ${BRANDING}`;
253
+ return {
254
+ html: htmlShell(title, body),
255
+ title,
256
+ type: "chart-3d",
257
+ };
258
+ }
259
+ // ── scene ────────────────────────────────────────────────────────────────────
260
+ function generateScene(input) {
261
+ const model = input.modelUrl || DEFAULT_MODEL;
262
+ const title = input.title || "3D Scene";
263
+ const opts = input.options || {};
264
+ const bg = opts.backgroundColor || "#0d1117";
265
+ const autoRotate = opts.autoRotate !== false;
266
+ const body = `
267
+ <style>
268
+ canvas{width:100%;height:100%;cursor:grab;display:block}
269
+ canvas:active{cursor:grabbing}
270
+ .scene-title{position:absolute;top:16px;left:16px;font-size:20px;font-weight:700;color:#fff;text-shadow:0 2px 12px rgba(0,0,0,0.6);z-index:10;letter-spacing:-0.3px}
271
+ .scene-info{position:absolute;top:44px;left:16px;font-size:12px;color:#aaa;z-index:10}
272
+ #status{position:absolute;top:64px;left:16px;font-size:11px;color:#8ab4f8;z-index:10}
273
+ .controls-hint{position:absolute;bottom:16px;left:16px;font-size:11px;color:#666;z-index:10}
274
+ </style>
275
+ <div class="scene-title">${escapeHtml(title)}</div>
276
+ <div class="scene-info">Interactive 3D Scene</div>
277
+ <div id="status">Loading Filament.js WASM...</div>
278
+ <canvas id="viewer"></canvas>
279
+ <div class="controls-hint">Drag to orbit &bull; Scroll to zoom &bull; Two-finger to pan</div>
280
+ ${BRANDING}
281
+ ${filamentRendererScript({ modelUrl: model, bgColor: hexToBgColor(bg), autoRotate, sunIntensity: 120000, fillIntensity: 35000, orbitRadius: 4.0, orbitHeight: 1.0 })}`;
282
+ return {
283
+ html: htmlShell(title, body),
284
+ title,
285
+ type: "scene",
286
+ };
287
+ }
288
+ // ── product-360 ──────────────────────────────────────────────────────────────
289
+ function generateProduct360(input) {
290
+ const model = input.modelUrl || DEFAULT_MODEL;
291
+ const title = input.title || "Product 360\u00B0 View";
292
+ const opts = input.options || {};
293
+ const bg = opts.backgroundColor || "#0d1117";
294
+ const hotspots = input.hotspots || [];
295
+ // Product-360 always auto-rotates (with a slower speed handled in the core script)
296
+ const autoRotate = true;
297
+ const hotspotHtml = hotspots.length > 0 ? `
298
+ <div class="hotspots-overlay" id="hotspots">
299
+ ${hotspots.map((h, i) => `
300
+ <div class="hotspot" id="hotspot-${i}" data-position="${escapeHtml(h.position)}" data-normal="${escapeHtml(h.normal)}">
301
+ <div class="hotspot-dot"></div>
302
+ <div class="hotspot-annotation">
303
+ <div class="hotspot-title">${escapeHtml(h.label)}</div>
304
+ ${h.description ? `<div class="hotspot-desc">${escapeHtml(h.description)}</div>` : ""}
305
+ </div>
306
+ </div>`).join("\n")}
307
+ </div>` : "";
308
+ const body = `
309
+ <style>
310
+ canvas{width:100%;height:100%;cursor:grab;display:block}
311
+ canvas:active{cursor:grabbing}
312
+ .product-header{position:absolute;top:0;left:0;right:0;padding:16px 20px;background:linear-gradient(180deg,rgba(0,0,0,0.6),transparent);z-index:10}
313
+ .product-title{font-size:20px;font-weight:700;color:#fff}
314
+ .product-subtitle{font-size:12px;color:#aaa;margin-top:2px}
315
+ #status{position:absolute;top:70px;left:20px;font-size:11px;color:#8ab4f8;z-index:10}
316
+ .hotspots-overlay{position:absolute;top:0;left:0;right:0;bottom:0;pointer-events:none;z-index:5}
317
+ .hotspot{position:absolute;pointer-events:auto;cursor:pointer}
318
+ .hotspot-dot{width:24px;height:24px;border-radius:50%;border:2px solid #fff;background:rgba(66,133,244,0.8);box-shadow:0 2px 8px rgba(0,0,0,0.4);transition:transform 0.2s}
319
+ .hotspot:hover .hotspot-dot{transform:scale(1.3)}
320
+ .hotspot-annotation{position:absolute;bottom:32px;left:50%;transform:translateX(-50%);background:rgba(30,30,50,0.95);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:8px 12px;white-space:nowrap;pointer-events:none;opacity:0;transition:opacity 0.2s}
321
+ .hotspot:hover .hotspot-annotation{opacity:1}
322
+ .hotspot-title{font-size:13px;font-weight:600;color:#fff}
323
+ .hotspot-desc{font-size:11px;color:#aaa;margin-top:2px}
324
+ .rotate-hint{position:absolute;bottom:16px;left:20px;font-size:11px;color:#666;z-index:10}
325
+ </style>
326
+ <div class="product-header">
327
+ <div class="product-title">${escapeHtml(title)}</div>
328
+ <div class="product-subtitle">Drag to rotate &bull; Pinch to zoom${hotspots.length > 0 ? " &bull; Tap hotspots for details" : ""}</div>
329
+ </div>
330
+ <div id="status">Loading Filament.js WASM...</div>
331
+ <canvas id="viewer"></canvas>
332
+ ${hotspotHtml}
333
+ <div class="rotate-hint">360&deg; interactive view</div>
334
+ ${BRANDING}
335
+ ${filamentRendererScript({ modelUrl: model, bgColor: hexToBgColor(bg), autoRotate })}`;
336
+ return {
337
+ html: htmlShell(title, body),
338
+ title,
339
+ type: "product-360",
340
+ };
341
+ }
342
+ // ─── Utilities ───────────────────────────────────────────────────────────────
343
+ function formatNumber(n) {
344
+ if (n >= 1_000_000)
345
+ return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
346
+ if (n >= 1_000)
347
+ return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
348
+ return n.toLocaleString("en-US");
349
+ }
350
+ /** Darken a hex colour by a fraction (0..1). */
351
+ function darken(hex, amount) {
352
+ const rgb = hexToRgb(hex);
353
+ return rgbToHex(Math.round(rgb.r * (1 - amount)), Math.round(rgb.g * (1 - amount)), Math.round(rgb.b * (1 - amount)));
354
+ }
355
+ /** Lighten a hex colour by a fraction (0..1). */
356
+ function lighten(hex, amount) {
357
+ const rgb = hexToRgb(hex);
358
+ return rgbToHex(Math.min(255, Math.round(rgb.r + (255 - rgb.r) * amount)), Math.min(255, Math.round(rgb.g + (255 - rgb.g) * amount)), Math.min(255, Math.round(rgb.b + (255 - rgb.b) * amount)));
359
+ }
360
+ function hexToRgb(hex) {
361
+ const h = hex.replace("#", "");
362
+ return {
363
+ r: parseInt(h.substring(0, 2), 16),
364
+ g: parseInt(h.substring(2, 4), 16),
365
+ b: parseInt(h.substring(4, 6), 16),
366
+ };
367
+ }
368
+ function rgbToHex(r, g, b) {
369
+ return "#" + [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("");
370
+ }
371
+ // ─── Format response ─────────────────────────────────────────────────────────
372
+ export function formatArtifactResponse(result) {
373
+ const lines = [
374
+ `## ${result.title}`,
375
+ ``,
376
+ `Here is your interactive 3D ${result.type === "chart-3d" ? "chart" : "content"} powered by Filament.js (same engine as SceneView Android):`,
377
+ ``,
378
+ `\`\`\`html`,
379
+ result.html,
380
+ `\`\`\``,
381
+ ``,
382
+ `**How to use this:**`,
383
+ `- Copy the HTML above into a file and open in a browser`,
384
+ `- Or paste into any HTML preview tool`,
385
+ `- Drag to orbit, scroll/pinch to zoom`,
386
+ `- Uses Filament.js WASM for real-time PBR rendering`,
387
+ ``,
388
+ `---`,
389
+ `*Powered by SceneView + Filament.js \u2014 3D & AR for every platform*`,
390
+ ];
391
+ return lines.join("\n");
392
+ }
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import { fetchKnownIssues } from "./issues.js";
12
12
  import { parseNodeSections, findNodeSection, listNodeTypes } from "./node-reference.js";
13
13
  import { PLATFORM_ROADMAP, BEST_PRACTICES, AR_SETUP_GUIDE, TROUBLESHOOTING_GUIDE } from "./guides.js";
14
14
  import { buildPreviewUrl, validatePreviewInput, formatPreviewResponse } from "./preview.js";
15
+ import { validateArtifactInput, generateArtifact, formatArtifactResponse } from "./artifact.js";
15
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
17
  // ─── Legal disclaimer ─────────────────────────────────────────────────────────
17
18
  const DISCLAIMER = '\n\n---\n*Generated code suggestion. Review before use in production. See [TERMS.md](https://github.com/SceneView/sceneview/blob/main/mcp/TERMS.md).*';
@@ -32,7 +33,7 @@ catch {
32
33
  API_DOCS = "SceneView API docs not found. Run `npm run prepare` to bundle llms.txt.";
33
34
  }
34
35
  const NODE_SECTIONS = parseNodeSections(API_DOCS);
35
- const server = new Server({ name: "@sceneview/mcp", version: "3.3.0" }, { capabilities: { resources: {}, tools: {} } });
36
+ const server = new Server({ name: "@sceneview/mcp", version: "3.4.9" }, { capabilities: { resources: {}, tools: {} } });
36
37
  // ─── Resources ───────────────────────────────────────────────────────────────
37
38
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
38
39
  resources: [
@@ -245,6 +246,66 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
245
246
  required: [],
246
247
  },
247
248
  },
249
+ {
250
+ name: "create_3d_artifact",
251
+ description: 'Generates a complete, self-contained HTML page with interactive 3D content that Claude can render as an artifact. Returns valid HTML using model-viewer (Google\'s web component for 3D). Use this when the user asks to "show", "preview", "visualize" 3D models, create 3D charts/dashboards, or view products in 360°. The HTML works standalone in any browser, supports AR on mobile, and includes orbit controls. Types: "model-viewer" for 3D model viewing, "chart-3d" for 3D data visualization (bar charts with perspective), "scene" for rich 3D scenes with lighting, "product-360" for product turntables with hotspot annotations.',
252
+ inputSchema: {
253
+ type: "object",
254
+ properties: {
255
+ type: {
256
+ type: "string",
257
+ enum: ["model-viewer", "chart-3d", "scene", "product-360"],
258
+ description: '"model-viewer": interactive 3D model viewer with orbit + AR. "chart-3d": 3D bar chart for data visualization (revenue, analytics). "scene": rich 3D scene with lighting and environment. "product-360": product turntable with hotspot annotations + AR.',
259
+ },
260
+ modelUrl: {
261
+ type: "string",
262
+ description: "Public HTTPS URL to a .glb or .gltf model. If omitted, a default model is used. Not needed for chart-3d type.",
263
+ },
264
+ title: {
265
+ type: "string",
266
+ description: "Title displayed on the artifact. Defaults to a sensible name per type.",
267
+ },
268
+ data: {
269
+ type: "array",
270
+ items: {
271
+ type: "object",
272
+ properties: {
273
+ label: { type: "string", description: "Data point label (e.g. 'Q1 2025')" },
274
+ value: { type: "number", description: "Numeric value" },
275
+ color: { type: "string", description: "Optional hex color (e.g. '#4285F4'). Auto-assigned if omitted." },
276
+ },
277
+ required: ["label", "value"],
278
+ },
279
+ description: 'Data array for chart-3d type. Each item has {label, value, color?}. Required for "chart-3d", ignored for other types.',
280
+ },
281
+ options: {
282
+ type: "object",
283
+ properties: {
284
+ autoRotate: { type: "boolean", description: "Auto-rotate the model (default: true)" },
285
+ ar: { type: "boolean", description: "Enable AR on mobile devices (default: true)" },
286
+ backgroundColor: { type: "string", description: "Background color as hex (default: '#1a1a2e')" },
287
+ cameraOrbit: { type: "string", description: "Camera orbit string (default: '0deg 75deg 105%')" },
288
+ },
289
+ description: "Visual options for the 3D artifact.",
290
+ },
291
+ hotspots: {
292
+ type: "array",
293
+ items: {
294
+ type: "object",
295
+ properties: {
296
+ position: { type: "string", description: 'Hotspot 3D position, e.g. "0.5 1.2 0.3"' },
297
+ normal: { type: "string", description: 'Hotspot surface normal, e.g. "0 1 0"' },
298
+ label: { type: "string", description: "Hotspot label" },
299
+ description: { type: "string", description: "Hotspot description" },
300
+ },
301
+ required: ["position", "normal", "label"],
302
+ },
303
+ description: "Annotation hotspots for product-360 type. Each has position, normal, label, and optional description.",
304
+ },
305
+ },
306
+ required: ["type"],
307
+ },
308
+ },
248
309
  ],
249
310
  }));
250
311
  // ─── Tool handlers ────────────────────────────────────────────────────────────
@@ -753,6 +814,27 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
753
814
  const text = formatPreviewResponse(result);
754
815
  return { content: withDisclaimer([{ type: "text", text }]) };
755
816
  }
817
+ // ── create_3d_artifact ───────────────────────────────────────────────────
818
+ case "create_3d_artifact": {
819
+ const artifactInput = {
820
+ type: request.params.arguments?.type,
821
+ modelUrl: request.params.arguments?.modelUrl,
822
+ title: request.params.arguments?.title,
823
+ data: request.params.arguments?.data,
824
+ options: request.params.arguments?.options,
825
+ hotspots: request.params.arguments?.hotspots,
826
+ };
827
+ const validationError = validateArtifactInput(artifactInput);
828
+ if (validationError) {
829
+ return {
830
+ content: [{ type: "text", text: `Error: ${validationError}` }],
831
+ isError: true,
832
+ };
833
+ }
834
+ const result = generateArtifact(artifactInput);
835
+ const text = formatArtifactResponse(result);
836
+ return { content: withDisclaimer([{ type: "text", text }]) };
837
+ }
756
838
  default:
757
839
  return {
758
840
  content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
package/llms.txt CHANGED
@@ -7,7 +7,7 @@ SceneView is a declarative 3D and AR SDK for Android (Jetpack Compose, Filament,
7
7
  - AR + 3D: `io.github.sceneview:arsceneview:3.3.0`
8
8
 
9
9
  **Apple (iOS 17+ / macOS 14+ / visionOS 1+) — Swift Package:**
10
- - `https://github.com/SceneView/SceneViewSwift.git` (from: "3.3.0")
10
+ - `https://github.com/sceneview/sceneview-swift.git` (from: "3.3.0")
11
11
 
12
12
  **Min SDK:** 24 | **Target SDK:** 36 | **Kotlin:** 2.3.10 | **Compose BOM compatible**
13
13
 
@@ -1176,7 +1176,7 @@ React Native (Turbo Module / Fabric), KMP Compose iOS (UIKitView).
1176
1176
  ```swift
1177
1177
  // Package.swift
1178
1178
  dependencies: [
1179
- .package(url: "https://github.com/SceneView/SceneViewSwift.git", from: "3.3.0")
1179
+ .package(url: "https://github.com/sceneview/sceneview-swift.git", from: "3.3.0")
1180
1180
  ]
1181
1181
  ```
1182
1182
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sceneview-mcp",
3
- "version": "3.4.8",
3
+ "version": "3.4.10",
4
4
  "mcpName": "io.github.sceneview/mcp",
5
5
  "description": "MCP server for SceneView — cross-platform 3D & AR SDK for Android and iOS. Give Claude the full SceneView SDK so it writes correct, compilable code.",
6
6
  "keywords": [
@@ -22,15 +22,15 @@
22
22
  "ai"
23
23
  ],
24
24
  "license": "MIT",
25
- "author": "SceneView (https://github.com/SceneView)",
25
+ "author": "SceneView (https://github.com/sceneview)",
26
26
  "repository": {
27
27
  "type": "git",
28
- "url": "https://github.com/SceneView/sceneview.git",
28
+ "url": "https://github.com/sceneview/sceneview.git",
29
29
  "directory": "mcp"
30
30
  },
31
- "homepage": "https://github.com/SceneView/sceneview/tree/main/mcp#readme",
31
+ "homepage": "https://github.com/sceneview/sceneview/tree/main/mcp#readme",
32
32
  "bugs": {
33
- "url": "https://github.com/SceneView/sceneview/issues"
33
+ "url": "https://github.com/sceneview/sceneview/issues"
34
34
  },
35
35
  "type": "module",
36
36
  "bin": {
@@ -44,6 +44,7 @@
44
44
  "dist/node-reference.js",
45
45
  "dist/samples.js",
46
46
  "dist/preview.js",
47
+ "dist/artifact.js",
47
48
  "dist/validator.js",
48
49
  "llms.txt"
49
50
  ],