sceneview-mcp 3.4.8 → 3.4.9
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/artifact.js +343 -0
- package/dist/index.js +83 -1
- package/package.json +2 -1
package/dist/artifact.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
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
|
+
// Types:
|
|
7
|
+
// - model-viewer: interactive 3D model viewer with orbit controls + AR
|
|
8
|
+
// - chart-3d: 3D bar/pie charts for data visualization
|
|
9
|
+
// - scene: multi-model 3D scene with lighting and environment
|
|
10
|
+
// - product-360: product turntable with hotspot annotations + AR
|
|
11
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
12
|
+
const MODEL_VIEWER_CDN = "https://ajax.googleapis.com/ajax/libs/model-viewer/4.0.0/model-viewer.min.js";
|
|
13
|
+
const DEFAULT_MODEL = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/DamagedHelmet/glTF-Binary/DamagedHelmet.glb";
|
|
14
|
+
const DEFAULT_COLORS = [
|
|
15
|
+
"#4285F4", "#EA4335", "#FBBC04", "#34A853", "#FF6D01",
|
|
16
|
+
"#46BDC6", "#7BAAF7", "#F07B72", "#FCD04F", "#57BB8A",
|
|
17
|
+
];
|
|
18
|
+
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>`;
|
|
19
|
+
// ─── Shared HTML skeleton ────────────────────────────────────────────────────
|
|
20
|
+
function htmlShell(title, body, extraHead = "") {
|
|
21
|
+
return `<!DOCTYPE html>
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="utf-8">
|
|
25
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
26
|
+
<title>${escapeHtml(title)}</title>
|
|
27
|
+
<style>
|
|
28
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
29
|
+
html,body{width:100%;height:100%;overflow:hidden;background:#1a1a2e;color:#e0e0e0;font-family:system-ui,-apple-system,sans-serif}
|
|
30
|
+
</style>
|
|
31
|
+
${extraHead}
|
|
32
|
+
</head>
|
|
33
|
+
<body>
|
|
34
|
+
${body}
|
|
35
|
+
</body>
|
|
36
|
+
</html>`;
|
|
37
|
+
}
|
|
38
|
+
function escapeHtml(s) {
|
|
39
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
40
|
+
}
|
|
41
|
+
// ─── Validation ──────────────────────────────────────────────────────────────
|
|
42
|
+
export function validateArtifactInput(input) {
|
|
43
|
+
const validTypes = ["model-viewer", "chart-3d", "scene", "product-360"];
|
|
44
|
+
if (!validTypes.includes(input.type)) {
|
|
45
|
+
return `Invalid type "${input.type}". Must be one of: ${validTypes.join(", ")}`;
|
|
46
|
+
}
|
|
47
|
+
if (input.type === "chart-3d") {
|
|
48
|
+
if (!input.data || !Array.isArray(input.data) || input.data.length === 0) {
|
|
49
|
+
return 'Type "chart-3d" requires a non-empty `data` array with {label, value} objects.';
|
|
50
|
+
}
|
|
51
|
+
for (const d of input.data) {
|
|
52
|
+
if (typeof d.label !== "string" || typeof d.value !== "number") {
|
|
53
|
+
return "Each data item must have a string `label` and numeric `value`.";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (input.modelUrl) {
|
|
58
|
+
if (!input.modelUrl.startsWith("https://") && !input.modelUrl.startsWith("http://")) {
|
|
59
|
+
return "modelUrl must be an HTTP(S) URL.";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// ─── Generators ──────────────────────────────────────────────────────────────
|
|
65
|
+
export function generateArtifact(input) {
|
|
66
|
+
switch (input.type) {
|
|
67
|
+
case "model-viewer":
|
|
68
|
+
return generateModelViewer(input);
|
|
69
|
+
case "chart-3d":
|
|
70
|
+
return generateChart3D(input);
|
|
71
|
+
case "scene":
|
|
72
|
+
return generateScene(input);
|
|
73
|
+
case "product-360":
|
|
74
|
+
return generateProduct360(input);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// ── model-viewer ─────────────────────────────────────────────────────────────
|
|
78
|
+
function generateModelViewer(input) {
|
|
79
|
+
const model = input.modelUrl || DEFAULT_MODEL;
|
|
80
|
+
const title = input.title || "3D Model Viewer";
|
|
81
|
+
const opts = input.options || {};
|
|
82
|
+
const bg = opts.backgroundColor || "#1a1a2e";
|
|
83
|
+
const orbit = opts.cameraOrbit || "0deg 75deg 105%";
|
|
84
|
+
const autoRotate = opts.autoRotate !== false;
|
|
85
|
+
const ar = opts.ar !== false;
|
|
86
|
+
const arAttrs = ar
|
|
87
|
+
? `ar ar-modes="webxr scene-viewer quick-look"`
|
|
88
|
+
: "";
|
|
89
|
+
const rotateAttr = autoRotate ? `auto-rotate auto-rotate-delay="0"` : "";
|
|
90
|
+
const body = `
|
|
91
|
+
<style>
|
|
92
|
+
model-viewer{width:100%;height:100%;background:${bg};--poster-color:${bg}}
|
|
93
|
+
.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}
|
|
94
|
+
.ar-btn{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);padding:10px 24px;background:rgba(66,133,244,0.9);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:500;cursor:pointer;backdrop-filter:blur(8px);z-index:10}
|
|
95
|
+
.ar-btn:hover{background:rgba(66,133,244,1)}
|
|
96
|
+
</style>
|
|
97
|
+
<script type="module" src="${MODEL_VIEWER_CDN}"></script>
|
|
98
|
+
<div class="title">${escapeHtml(title)}</div>
|
|
99
|
+
<model-viewer
|
|
100
|
+
src="${escapeHtml(model)}"
|
|
101
|
+
camera-controls
|
|
102
|
+
touch-action="pan-y"
|
|
103
|
+
camera-orbit="${escapeHtml(orbit)}"
|
|
104
|
+
shadow-intensity="1"
|
|
105
|
+
shadow-softness="0.5"
|
|
106
|
+
exposure="1"
|
|
107
|
+
environment-image="neutral"
|
|
108
|
+
${rotateAttr}
|
|
109
|
+
${arAttrs}
|
|
110
|
+
style="width:100%;height:100%"
|
|
111
|
+
>
|
|
112
|
+
${ar ? `<button slot="ar-button" class="ar-btn">View in AR</button>` : ""}
|
|
113
|
+
</model-viewer>
|
|
114
|
+
${BRANDING}`;
|
|
115
|
+
return {
|
|
116
|
+
html: htmlShell(title, body),
|
|
117
|
+
title,
|
|
118
|
+
type: "model-viewer",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
// ── chart-3d ─────────────────────────────────────────────────────────────────
|
|
122
|
+
function generateChart3D(input) {
|
|
123
|
+
const title = input.title || "3D Data Visualization";
|
|
124
|
+
const data = input.data;
|
|
125
|
+
const bg = input.options?.backgroundColor || "#1a1a2e";
|
|
126
|
+
const maxVal = Math.max(...data.map((d) => d.value));
|
|
127
|
+
const bars = data
|
|
128
|
+
.map((d, i) => {
|
|
129
|
+
const color = d.color || DEFAULT_COLORS[i % DEFAULT_COLORS.length];
|
|
130
|
+
const heightPct = maxVal > 0 ? (d.value / maxVal) * 100 : 0;
|
|
131
|
+
const height = Math.max(heightPct * 2.5, 8); // px scale, min 8px
|
|
132
|
+
return `
|
|
133
|
+
<div class="bar-group" style="--delay:${i * 0.08}s">
|
|
134
|
+
<div class="bar-wrapper">
|
|
135
|
+
<div class="bar-value">${formatNumber(d.value)}</div>
|
|
136
|
+
<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">
|
|
137
|
+
<div class="bar-face-top" style="background:${lighten(color, 0.15)}"></div>
|
|
138
|
+
<div class="bar-face-right" style="background:${darken(color, 0.2)}"></div>
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="bar-label">${escapeHtml(d.label)}</div>
|
|
142
|
+
</div>`;
|
|
143
|
+
})
|
|
144
|
+
.join("\n");
|
|
145
|
+
const body = `
|
|
146
|
+
<style>
|
|
147
|
+
body{background:${bg};display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;perspective:800px;overflow:hidden}
|
|
148
|
+
.chart-title{font-size:22px;font-weight:700;margin-bottom:24px;color:#fff;letter-spacing:-0.5px}
|
|
149
|
+
.chart-subtitle{font-size:13px;color:#888;margin-bottom:32px}
|
|
150
|
+
.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}
|
|
151
|
+
.bar-group{display:flex;flex-direction:column;align-items:center;animation:barIn 0.6s ease-out var(--delay) both}
|
|
152
|
+
.bar-wrapper{position:relative;display:flex;flex-direction:column;align-items:center}
|
|
153
|
+
.bar{width:clamp(28px,6vw,56px);border-radius:4px 4px 0 0;position:relative;transform-style:preserve-3d;transition:height 0.5s ease-out}
|
|
154
|
+
.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}
|
|
155
|
+
.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}
|
|
156
|
+
.bar-value{font-size:12px;font-weight:600;color:#fff;margin-bottom:6px;white-space:nowrap}
|
|
157
|
+
.bar-label{font-size:11px;color:#aaa;margin-top:8px;text-align:center;max-width:72px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
158
|
+
@keyframes barIn{from{opacity:0;transform:scaleY(0) translateY(20px)}to{opacity:1;transform:scaleY(1) translateY(0)}}
|
|
159
|
+
.total-row{margin-top:24px;font-size:14px;color:#aaa}
|
|
160
|
+
.total-row strong{color:#fff;font-size:16px}
|
|
161
|
+
</style>
|
|
162
|
+
<div class="chart-title">${escapeHtml(title)}</div>
|
|
163
|
+
<div class="chart-subtitle">${data.length} data points</div>
|
|
164
|
+
<div class="chart-container">
|
|
165
|
+
${bars}
|
|
166
|
+
</div>
|
|
167
|
+
<div class="total-row">Total: <strong>${formatNumber(data.reduce((s, d) => s + d.value, 0))}</strong></div>
|
|
168
|
+
${BRANDING}`;
|
|
169
|
+
return {
|
|
170
|
+
html: htmlShell(title, body),
|
|
171
|
+
title,
|
|
172
|
+
type: "chart-3d",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// ── scene ────────────────────────────────────────────────────────────────────
|
|
176
|
+
function generateScene(input) {
|
|
177
|
+
const model = input.modelUrl || DEFAULT_MODEL;
|
|
178
|
+
const title = input.title || "3D Scene";
|
|
179
|
+
const opts = input.options || {};
|
|
180
|
+
const bg = opts.backgroundColor || "#1a1a2e";
|
|
181
|
+
const autoRotate = opts.autoRotate !== false;
|
|
182
|
+
const ar = opts.ar !== false;
|
|
183
|
+
const orbit = opts.cameraOrbit || "0deg 75deg 150%";
|
|
184
|
+
const arAttrs = ar ? `ar ar-modes="webxr scene-viewer quick-look"` : "";
|
|
185
|
+
const rotateAttr = autoRotate ? `auto-rotate auto-rotate-delay="0"` : "";
|
|
186
|
+
const body = `
|
|
187
|
+
<style>
|
|
188
|
+
model-viewer{width:100%;height:100%;background:${bg};--poster-color:${bg}}
|
|
189
|
+
.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}
|
|
190
|
+
.scene-info{position:absolute;top:44px;left:16px;font-size:12px;color:#aaa;z-index:10}
|
|
191
|
+
.controls-hint{position:absolute;bottom:16px;left:16px;font-size:11px;color:#666;z-index:10}
|
|
192
|
+
.ar-btn{position:absolute;bottom:16px;right:16px;padding:10px 20px;background:rgba(66,133,244,0.9);color:#fff;border:none;border-radius:24px;font-size:13px;font-weight:500;cursor:pointer;z-index:10}
|
|
193
|
+
</style>
|
|
194
|
+
<script type="module" src="${MODEL_VIEWER_CDN}"></script>
|
|
195
|
+
<div class="scene-title">${escapeHtml(title)}</div>
|
|
196
|
+
<div class="scene-info">Interactive 3D Scene</div>
|
|
197
|
+
<model-viewer
|
|
198
|
+
src="${escapeHtml(model)}"
|
|
199
|
+
camera-controls
|
|
200
|
+
touch-action="pan-y"
|
|
201
|
+
camera-orbit="${escapeHtml(orbit)}"
|
|
202
|
+
shadow-intensity="1.5"
|
|
203
|
+
shadow-softness="0.8"
|
|
204
|
+
exposure="1.1"
|
|
205
|
+
environment-image="neutral"
|
|
206
|
+
tone-mapping="commerce"
|
|
207
|
+
${rotateAttr}
|
|
208
|
+
${arAttrs}
|
|
209
|
+
style="width:100%;height:100%"
|
|
210
|
+
>
|
|
211
|
+
${ar ? `<button slot="ar-button" class="ar-btn">View in AR</button>` : ""}
|
|
212
|
+
</model-viewer>
|
|
213
|
+
<div class="controls-hint">Drag to orbit • Scroll to zoom • Two-finger to pan</div>
|
|
214
|
+
${BRANDING}`;
|
|
215
|
+
return {
|
|
216
|
+
html: htmlShell(title, body),
|
|
217
|
+
title,
|
|
218
|
+
type: "scene",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// ── product-360 ──────────────────────────────────────────────────────────────
|
|
222
|
+
function generateProduct360(input) {
|
|
223
|
+
const model = input.modelUrl || DEFAULT_MODEL;
|
|
224
|
+
const title = input.title || "Product 360\u00B0 View";
|
|
225
|
+
const opts = input.options || {};
|
|
226
|
+
const bg = opts.backgroundColor || "#1a1a2e";
|
|
227
|
+
const orbit = opts.cameraOrbit || "0deg 75deg 105%";
|
|
228
|
+
const ar = opts.ar !== false;
|
|
229
|
+
const hotspots = input.hotspots || [];
|
|
230
|
+
const hotspotHtml = hotspots
|
|
231
|
+
.map((h, i) => `
|
|
232
|
+
<button class="hotspot" slot="hotspot-${i}"
|
|
233
|
+
data-position="${escapeHtml(h.position)}"
|
|
234
|
+
data-normal="${escapeHtml(h.normal)}"
|
|
235
|
+
data-visibility-attribute="visible">
|
|
236
|
+
<div class="hotspot-annotation">
|
|
237
|
+
<div class="hotspot-title">${escapeHtml(h.label)}</div>
|
|
238
|
+
${h.description ? `<div class="hotspot-desc">${escapeHtml(h.description)}</div>` : ""}
|
|
239
|
+
</div>
|
|
240
|
+
</button>`)
|
|
241
|
+
.join("\n");
|
|
242
|
+
const body = `
|
|
243
|
+
<style>
|
|
244
|
+
model-viewer{width:100%;height:100%;background:${bg};--poster-color:${bg}}
|
|
245
|
+
.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}
|
|
246
|
+
.product-title{font-size:20px;font-weight:700;color:#fff}
|
|
247
|
+
.product-subtitle{font-size:12px;color:#aaa;margin-top:2px}
|
|
248
|
+
.hotspot{display:block;width:24px;height:24px;border-radius:50%;border:2px solid #fff;background:rgba(66,133,244,0.8);cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.4);transition:transform 0.2s}
|
|
249
|
+
.hotspot:hover{transform:scale(1.3)}
|
|
250
|
+
.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}
|
|
251
|
+
.hotspot:hover .hotspot-annotation{opacity:1}
|
|
252
|
+
.hotspot-title{font-size:13px;font-weight:600;color:#fff}
|
|
253
|
+
.hotspot-desc{font-size:11px;color:#aaa;margin-top:2px}
|
|
254
|
+
.ar-strip{position:absolute;bottom:0;left:0;right:0;padding:12px 20px;background:linear-gradient(0deg,rgba(0,0,0,0.6),transparent);display:flex;align-items:center;justify-content:space-between;z-index:10}
|
|
255
|
+
.ar-btn{padding:10px 24px;background:rgba(66,133,244,0.9);color:#fff;border:none;border-radius:24px;font-size:14px;font-weight:500;cursor:pointer}
|
|
256
|
+
.ar-btn:hover{background:rgba(66,133,244,1)}
|
|
257
|
+
.rotate-hint{font-size:11px;color:#666}
|
|
258
|
+
</style>
|
|
259
|
+
<script type="module" src="${MODEL_VIEWER_CDN}"></script>
|
|
260
|
+
<div class="product-header">
|
|
261
|
+
<div class="product-title">${escapeHtml(title)}</div>
|
|
262
|
+
<div class="product-subtitle">Drag to rotate • Pinch to zoom${hotspots.length > 0 ? " • Tap hotspots for details" : ""}</div>
|
|
263
|
+
</div>
|
|
264
|
+
<model-viewer
|
|
265
|
+
src="${escapeHtml(model)}"
|
|
266
|
+
camera-controls
|
|
267
|
+
touch-action="pan-y"
|
|
268
|
+
camera-orbit="${escapeHtml(orbit)}"
|
|
269
|
+
auto-rotate
|
|
270
|
+
auto-rotate-delay="3000"
|
|
271
|
+
shadow-intensity="1"
|
|
272
|
+
shadow-softness="0.5"
|
|
273
|
+
exposure="1"
|
|
274
|
+
environment-image="neutral"
|
|
275
|
+
tone-mapping="commerce"
|
|
276
|
+
interaction-prompt="auto"
|
|
277
|
+
${ar ? `ar ar-modes="webxr scene-viewer quick-look"` : ""}
|
|
278
|
+
style="width:100%;height:100%"
|
|
279
|
+
>
|
|
280
|
+
${hotspotHtml}
|
|
281
|
+
${ar ? `<button slot="ar-button" class="ar-btn">View in Your Space</button>` : ""}
|
|
282
|
+
</model-viewer>
|
|
283
|
+
<div class="ar-strip">
|
|
284
|
+
<div class="rotate-hint">360° interactive view</div>
|
|
285
|
+
</div>
|
|
286
|
+
${BRANDING}`;
|
|
287
|
+
return {
|
|
288
|
+
html: htmlShell(title, body),
|
|
289
|
+
title,
|
|
290
|
+
type: "product-360",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// ─── Utilities ───────────────────────────────────────────────────────────────
|
|
294
|
+
function formatNumber(n) {
|
|
295
|
+
if (n >= 1_000_000)
|
|
296
|
+
return (n / 1_000_000).toFixed(1).replace(/\.0$/, "") + "M";
|
|
297
|
+
if (n >= 1_000)
|
|
298
|
+
return (n / 1_000).toFixed(1).replace(/\.0$/, "") + "K";
|
|
299
|
+
return n.toLocaleString("en-US");
|
|
300
|
+
}
|
|
301
|
+
/** Darken a hex colour by a fraction (0..1). */
|
|
302
|
+
function darken(hex, amount) {
|
|
303
|
+
const rgb = hexToRgb(hex);
|
|
304
|
+
return rgbToHex(Math.round(rgb.r * (1 - amount)), Math.round(rgb.g * (1 - amount)), Math.round(rgb.b * (1 - amount)));
|
|
305
|
+
}
|
|
306
|
+
/** Lighten a hex colour by a fraction (0..1). */
|
|
307
|
+
function lighten(hex, amount) {
|
|
308
|
+
const rgb = hexToRgb(hex);
|
|
309
|
+
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)));
|
|
310
|
+
}
|
|
311
|
+
function hexToRgb(hex) {
|
|
312
|
+
const h = hex.replace("#", "");
|
|
313
|
+
return {
|
|
314
|
+
r: parseInt(h.substring(0, 2), 16),
|
|
315
|
+
g: parseInt(h.substring(2, 4), 16),
|
|
316
|
+
b: parseInt(h.substring(4, 6), 16),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
function rgbToHex(r, g, b) {
|
|
320
|
+
return "#" + [r, g, b].map((c) => c.toString(16).padStart(2, "0")).join("");
|
|
321
|
+
}
|
|
322
|
+
// ─── Format response ─────────────────────────────────────────────────────────
|
|
323
|
+
export function formatArtifactResponse(result) {
|
|
324
|
+
const lines = [
|
|
325
|
+
`## ${result.title}`,
|
|
326
|
+
``,
|
|
327
|
+
`Here is your interactive 3D ${result.type === "chart-3d" ? "chart" : "content"}:`,
|
|
328
|
+
``,
|
|
329
|
+
`\`\`\`html`,
|
|
330
|
+
result.html,
|
|
331
|
+
`\`\`\``,
|
|
332
|
+
``,
|
|
333
|
+
`**How to use this:**`,
|
|
334
|
+
`- Copy the HTML above into a file and open in a browser`,
|
|
335
|
+
`- Or paste into any HTML preview tool`,
|
|
336
|
+
`- On mobile: tap "View in AR" to see it in your space`,
|
|
337
|
+
`- Drag to orbit, scroll/pinch to zoom`,
|
|
338
|
+
``,
|
|
339
|
+
`---`,
|
|
340
|
+
`*Powered by SceneView \u2014 3D & AR for every platform*`,
|
|
341
|
+
];
|
|
342
|
+
return lines.join("\n");
|
|
343
|
+
}
|
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.
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sceneview-mcp",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.9",
|
|
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": [
|
|
@@ -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
|
],
|