sceneview-mcp 3.6.2 → 3.6.5

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,137 @@
1
+ // ─── Stripe Billing Validation for SceneView MCP Pro ─────────────────────────
2
+ //
3
+ // Validates API keys against Stripe subscriptions to determine tier access.
4
+ // Uses native fetch() — no external Stripe SDK dependency.
5
+ // MCP servers log to stderr (stdout is reserved for the JSON-RPC protocol).
6
+ const cache = new Map();
7
+ const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
8
+ function getCached(apiKey) {
9
+ const entry = cache.get(apiKey);
10
+ if (!entry)
11
+ return undefined;
12
+ if (Date.now() - entry.cachedAt > CACHE_TTL_MS) {
13
+ cache.delete(apiKey);
14
+ return undefined;
15
+ }
16
+ return entry.status;
17
+ }
18
+ function setCache(apiKey, status) {
19
+ cache.set(apiKey, { status, cachedAt: Date.now() });
20
+ }
21
+ /** Clears the validation cache. Useful for testing. */
22
+ export function clearCache() {
23
+ cache.clear();
24
+ }
25
+ // ─── Development Allowlist ───────────────────────────────────────────────────
26
+ const DEV_ALLOWLIST = new Set([
27
+ "dev_test_key",
28
+ "sceneview_dev",
29
+ ]);
30
+ function checkDevAllowlist(apiKey) {
31
+ if (DEV_ALLOWLIST.has(apiKey)) {
32
+ return { valid: true, tier: "pro" };
33
+ }
34
+ return { valid: false, tier: "free", error: "Invalid API key (dev mode — no Stripe configured)" };
35
+ }
36
+ async function fetchStripeSubscription(subscriptionId, stripeSecretKey) {
37
+ const url = `https://api.stripe.com/v1/subscriptions/${encodeURIComponent(subscriptionId)}`;
38
+ let response;
39
+ try {
40
+ response = await fetch(url, {
41
+ method: "GET",
42
+ headers: {
43
+ Authorization: `Bearer ${stripeSecretKey}`,
44
+ "Content-Type": "application/x-www-form-urlencoded",
45
+ },
46
+ });
47
+ }
48
+ catch (err) {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ return { valid: false, tier: "free", error: `Stripe API request failed: ${message}` };
51
+ }
52
+ if (!response.ok) {
53
+ const body = await response.text().catch(() => "unknown error");
54
+ if (response.status === 404) {
55
+ return { valid: false, tier: "free", error: "Subscription not found" };
56
+ }
57
+ return {
58
+ valid: false,
59
+ tier: "free",
60
+ error: `Stripe API error (${response.status}): ${body}`,
61
+ };
62
+ }
63
+ let subscription;
64
+ try {
65
+ subscription = (await response.json());
66
+ }
67
+ catch {
68
+ return { valid: false, tier: "free", error: "Failed to parse Stripe response" };
69
+ }
70
+ const isActive = subscription.status === "active" || subscription.status === "trialing";
71
+ const expiresAt = new Date(subscription.current_period_end * 1000).toISOString();
72
+ if (isActive) {
73
+ return {
74
+ valid: true,
75
+ tier: "pro",
76
+ customerId: subscription.customer,
77
+ expiresAt,
78
+ };
79
+ }
80
+ return {
81
+ valid: false,
82
+ tier: "free",
83
+ customerId: subscription.customer,
84
+ expiresAt,
85
+ error: `Subscription status is "${subscription.status}" (expected "active" or "trialing")`,
86
+ };
87
+ }
88
+ // ─── Public API ──────────────────────────────────────────────────────────────
89
+ /**
90
+ * Validates an API key by checking it against Stripe subscriptions.
91
+ *
92
+ * - Results are cached in memory for 5 minutes to avoid excessive Stripe calls.
93
+ * - If `STRIPE_SECRET_KEY` is not set, falls back to a development allowlist.
94
+ * - The API key is treated as a Stripe subscription ID for the lookup.
95
+ */
96
+ export async function validateApiKey(apiKey) {
97
+ // Check cache first
98
+ const cached = getCached(apiKey);
99
+ if (cached)
100
+ return cached;
101
+ let result;
102
+ if (isDevMode()) {
103
+ // No Stripe key configured — use allowlist for development/testing
104
+ result = checkDevAllowlist(apiKey);
105
+ }
106
+ else {
107
+ const stripeKey = process.env.STRIPE_SECRET_KEY;
108
+ result = await fetchStripeSubscription(apiKey, stripeKey);
109
+ }
110
+ setCache(apiKey, result);
111
+ return result;
112
+ }
113
+ /**
114
+ * Returns the configured SceneView API key from the environment, or undefined
115
+ * if the user is on the free tier (no key set).
116
+ */
117
+ export function getConfiguredApiKey() {
118
+ return process.env.SCENEVIEW_API_KEY;
119
+ }
120
+ /**
121
+ * Returns true when `STRIPE_SECRET_KEY` is not set in the environment.
122
+ * In dev mode, API key validation falls back to a simple allowlist instead
123
+ * of calling the Stripe API.
124
+ */
125
+ export function isDevMode() {
126
+ return !process.env.STRIPE_SECRET_KEY;
127
+ }
128
+ /**
129
+ * Records usage of an MCP tool for a given API key.
130
+ *
131
+ * Stub for future billing/metering integration. Currently logs to stderr
132
+ * (MCP servers must not write to stdout — it carries the JSON-RPC protocol).
133
+ */
134
+ export async function recordUsage(apiKey, toolName) {
135
+ const timestamp = new Date().toISOString();
136
+ process.stderr.write(`[billing] ${timestamp} usage: key=${apiKey.slice(0, 8)}… tool=${toolName}\n`);
137
+ }
@@ -0,0 +1,302 @@
1
+ /**
2
+ * convert-platform.ts
3
+ *
4
+ * Convert SceneView code between Android (Kotlin/Compose) and iOS (Swift/SwiftUI).
5
+ * Also generates multiplatform code from a scene description.
6
+ */
7
+ const CONVERSION_RULES = [
8
+ {
9
+ pattern: /\bSceneView\s*\(/g,
10
+ androidToIos: "SceneView {",
11
+ iosToAndroid: "SceneView(",
12
+ description: "SceneView composable → SceneView SwiftUI view",
13
+ },
14
+ {
15
+ pattern: /\bARSceneView\s*\(/g,
16
+ androidToIos: "ARSceneView(",
17
+ iosToAndroid: "ARSceneView(",
18
+ description: "ARSceneView composable → ARSceneView SwiftUI view",
19
+ },
20
+ {
21
+ pattern: /rememberModelInstance\s*\(\s*modelLoader\s*,\s*"([^"]+)\.glb"\s*\)/g,
22
+ androidToIos: 'try await ModelNode.load("$1.usdz")',
23
+ iosToAndroid: 'rememberModelInstance(modelLoader, "$1.glb")',
24
+ description: "Model loading: rememberModelInstance → ModelNode.load, GLB → USDZ",
25
+ },
26
+ {
27
+ pattern: /rememberEngine\(\)/g,
28
+ androidToIos: "// Engine managed by RealityKit automatically",
29
+ iosToAndroid: "rememberEngine()",
30
+ description: "Engine: explicit in Android, implicit in iOS/RealityKit",
31
+ },
32
+ {
33
+ pattern: /rememberModelLoader\(engine\)/g,
34
+ androidToIos: "// ModelLoader not needed — RealityKit loads models directly",
35
+ iosToAndroid: "rememberModelLoader(engine)",
36
+ description: "ModelLoader: Android-specific, not needed on iOS",
37
+ },
38
+ {
39
+ pattern: /rememberEnvironmentLoader\(engine\)/g,
40
+ androidToIos: "// Environment managed by RealityKit automatically",
41
+ iosToAndroid: "rememberEnvironmentLoader(engine)",
42
+ description: "EnvironmentLoader: Android-specific",
43
+ },
44
+ {
45
+ pattern: /Modifier\.fillMaxSize\(\)/g,
46
+ androidToIos: ".edgesIgnoringSafeArea(.all)",
47
+ iosToAndroid: "Modifier.fillMaxSize()",
48
+ description: "Full-screen modifier",
49
+ },
50
+ {
51
+ pattern: /Position\(([^)]+)\)/g,
52
+ androidToIos: "SIMD3<Float>($1)",
53
+ iosToAndroid: "Position($1)",
54
+ description: "Position type: Position → SIMD3<Float>",
55
+ },
56
+ {
57
+ pattern: /\.glb\b/g,
58
+ androidToIos: ".usdz",
59
+ iosToAndroid: ".glb",
60
+ description: "Model format: GLB (Android) ↔ USDZ (iOS)",
61
+ },
62
+ ];
63
+ export function convertAndroidToIos(code) {
64
+ let result = code;
65
+ const changes = [];
66
+ const warnings = [];
67
+ for (const rule of CONVERSION_RULES) {
68
+ if (rule.pattern.test(result)) {
69
+ const replacement = typeof rule.androidToIos === "string" ? rule.androidToIos : rule.androidToIos;
70
+ result = result.replace(new RegExp(rule.pattern.source, rule.pattern.flags), replacement);
71
+ changes.push(rule.description);
72
+ }
73
+ }
74
+ // Add Swift-specific warnings
75
+ warnings.push("RealityKit uses USDZ models, not GLB/glTF. Convert models using Apple's Reality Converter or Blender.");
76
+ warnings.push("RealityKit handles engine, material, and environment lifecycle automatically — no manual management needed.");
77
+ warnings.push("Swift async/await: model loading must use `try await` inside `.task { }` block.");
78
+ if (code.includes("LightNode")) {
79
+ warnings.push("LightNode API differs: RealityKit uses DirectionalLightComponent, PointLightComponent, SpotLightComponent on Entity.");
80
+ }
81
+ if (code.includes("materialLoader")) {
82
+ warnings.push("Material API differs: RealityKit uses SimpleMaterial, PhysicallyBasedMaterial, or UnlitMaterial.");
83
+ }
84
+ return { code: result, sourceplatform: "android", targetPlatform: "ios", changes, warnings };
85
+ }
86
+ export function convertIosToAndroid(code) {
87
+ let result = code;
88
+ const changes = [];
89
+ const warnings = [];
90
+ // iOS → Android specific replacements
91
+ const iosPatterns = [
92
+ { pattern: /SceneView\s*\{/g, replacement: "SceneView(engine = engine) {", description: "SceneView → SceneView with engine" },
93
+ { pattern: /ARSceneView\s*\(/g, replacement: "ARSceneView(engine = engine, ", description: "ARSceneView → ARSceneView with engine" },
94
+ { pattern: /try\s+await\s+ModelNode\.load\s*\(\s*"([^"]+)\.usdz"\s*\)/g, replacement: 'rememberModelInstance(modelLoader, "$1.glb")', description: "ModelNode.load → rememberModelInstance, USDZ → GLB" },
95
+ { pattern: /import\s+SwiftUI/g, replacement: "// SwiftUI → Jetpack Compose", description: "Import replacement" },
96
+ { pattern: /import\s+SceneViewSwift/g, replacement: "import io.github.sceneview.*", description: "Import replacement" },
97
+ { pattern: /import\s+RealityKit/g, replacement: "// RealityKit → Filament (included in SceneView)", description: "Import replacement" },
98
+ { pattern: /\.task\s*\{/g, replacement: "LaunchedEffect(Unit) {", description: ".task → LaunchedEffect" },
99
+ { pattern: /\.edgesIgnoringSafeArea\(.all\)/g, replacement: "", description: "Remove iOS-specific modifier" },
100
+ { pattern: /\.usdz\b/g, replacement: ".glb", description: "Model format: USDZ → GLB" },
101
+ { pattern: /@State\s+private\s+var/g, replacement: "var /* @State */ ", description: "@State → Compose state" },
102
+ { pattern: /SIMD3<Float>\(([^)]+)\)/g, replacement: "Position($1)", description: "SIMD3 → Position" },
103
+ ];
104
+ for (const rule of iosPatterns) {
105
+ if (rule.pattern.test(result)) {
106
+ result = result.replace(rule.pattern, rule.replacement);
107
+ changes.push(rule.description);
108
+ }
109
+ }
110
+ warnings.push("Android uses GLB/glTF models, not USDZ. Convert models using Blender or gltf-transform.");
111
+ warnings.push("Android requires explicit engine, modelLoader, and environmentLoader management.");
112
+ warnings.push("Filament JNI calls must run on the main thread. Use rememberModelInstance in composables.");
113
+ if (code.includes("Entity") || code.includes("entity")) {
114
+ warnings.push("RealityKit Entity → SceneView Node types (ModelNode, LightNode, etc.).");
115
+ }
116
+ return { code: result, sourceplatform: "ios", targetPlatform: "android", changes, warnings };
117
+ }
118
+ export function generateMultiplatformCode(description) {
119
+ const lower = description.toLowerCase();
120
+ const isAR = lower.includes("ar") || lower.includes("augmented") || lower.includes("camera");
121
+ const hasModel = lower.includes("model") || lower.includes("object") || lower.includes("3d") || !isAR;
122
+ const modelName = "scene_object";
123
+ const androidCode = isAR
124
+ ? `@Composable
125
+ fun MultiplatformARScreen() {
126
+ val engine = rememberEngine()
127
+ val modelLoader = rememberModelLoader(engine)
128
+ val modelInstance = rememberModelInstance(modelLoader, "models/${modelName}.glb")
129
+ var anchor by remember { mutableStateOf<Anchor?>(null) }
130
+
131
+ ARSceneView(
132
+ modifier = Modifier.fillMaxSize(),
133
+ engine = engine,
134
+ modelLoader = modelLoader,
135
+ planeRenderer = true,
136
+ sessionConfiguration = { session, config ->
137
+ config.lightEstimationMode = Config.LightEstimationMode.ENVIRONMENTAL_HDR
138
+ config.planeFindingMode = Config.PlaneFindingMode.HORIZONTAL_AND_VERTICAL
139
+ },
140
+ onTouchEvent = { event, hitResult ->
141
+ if (event.action == MotionEvent.ACTION_UP && hitResult != null) {
142
+ anchor = hitResult.createAnchor()
143
+ }
144
+ true
145
+ }
146
+ ) {
147
+ anchor?.let { a ->
148
+ AnchorNode(anchor = a) {
149
+ modelInstance?.let { instance ->
150
+ ModelNode(
151
+ modelInstance = instance,
152
+ scaleToUnits = 0.5f,
153
+ isEditable = true
154
+ )
155
+ }
156
+ }
157
+ }
158
+ }
159
+ }`
160
+ : `@Composable
161
+ fun MultiplatformSceneScreen() {
162
+ val engine = rememberEngine()
163
+ val modelLoader = rememberModelLoader(engine)
164
+ val environmentLoader = rememberEnvironmentLoader(engine)
165
+ val modelInstance = rememberModelInstance(modelLoader, "models/${modelName}.glb")
166
+
167
+ SceneView(
168
+ modifier = Modifier.fillMaxSize(),
169
+ engine = engine,
170
+ modelLoader = modelLoader,
171
+ environment = rememberEnvironment(environmentLoader) {
172
+ environmentLoader.createHDREnvironment("environments/sky_2k.hdr")
173
+ ?: createEnvironment(environmentLoader)
174
+ },
175
+ mainLightNode = rememberMainLightNode(engine) { intensity = 100_000f },
176
+ cameraManipulator = rememberCameraManipulator()
177
+ ) {
178
+ modelInstance?.let { instance ->
179
+ ModelNode(
180
+ modelInstance = instance,
181
+ scaleToUnits = 1.0f,
182
+ centerOrigin = Position(0f, 0f, 0f),
183
+ isEditable = true
184
+ )
185
+ }
186
+ }
187
+ }`;
188
+ const iosCode = isAR
189
+ ? `import SwiftUI
190
+ import SceneViewSwift
191
+ import RealityKit
192
+
193
+ struct MultiplatformARView: View {
194
+ @State private var model: ModelNode?
195
+
196
+ var body: some View {
197
+ ARSceneView(
198
+ planeDetection: .horizontal,
199
+ showCoachingOverlay: true,
200
+ onTapOnPlane: { position, arView in
201
+ guard let model else { return }
202
+ let anchor = AnchorNode.world(position: position)
203
+ let clone = model.entity.clone(recursive: true)
204
+ clone.scale = .init(repeating: 0.5)
205
+ anchor.add(clone)
206
+ arView.scene.addAnchor(anchor.entity)
207
+ }
208
+ )
209
+ .edgesIgnoringSafeArea(.all)
210
+ .task {
211
+ do {
212
+ model = try await ModelNode.load("models/${modelName}.usdz")
213
+ } catch {
214
+ print("Failed to load model: \\(error)")
215
+ }
216
+ }
217
+ }
218
+ }`
219
+ : `import SwiftUI
220
+ import SceneViewSwift
221
+ import RealityKit
222
+
223
+ struct MultiplatformSceneView: View {
224
+ @State private var model: ModelNode?
225
+
226
+ var body: some View {
227
+ SceneView { root in
228
+ if let model {
229
+ root.addChild(model.entity)
230
+ }
231
+ }
232
+ .cameraControls(.orbit)
233
+ .task {
234
+ do {
235
+ model = try await ModelNode.load("models/${modelName}.usdz")
236
+ model?.scaleToUnits(1.0)
237
+ } catch {
238
+ print("Failed to load model: \\(error)")
239
+ }
240
+ }
241
+ }
242
+ }`;
243
+ return {
244
+ androidCode,
245
+ iosCode,
246
+ description,
247
+ notes: [
248
+ "Android uses GLB/glTF models, iOS uses USDZ format.",
249
+ "Both platforms render with PBR materials but use different engines (Filament vs RealityKit).",
250
+ "Android requires explicit engine/loader management; iOS/RealityKit handles this automatically.",
251
+ isAR ? "AR features: Android uses ARCore, iOS uses ARKit. Both support plane detection and anchoring." : "",
252
+ `Replace 'models/${modelName}.glb' and 'models/${modelName}.usdz' with your actual model files.`,
253
+ ].filter(Boolean),
254
+ };
255
+ }
256
+ export function formatConversionResult(result) {
257
+ const targetLabel = result.targetPlatform === "ios" ? "iOS (SwiftUI + RealityKit)" : "Android (Jetpack Compose + Filament)";
258
+ const lang = result.targetPlatform === "ios" ? "swift" : "kotlin";
259
+ const parts = [
260
+ `## Code Converted to ${targetLabel}`,
261
+ ``,
262
+ `**${result.changes.length} conversion(s) applied.**`,
263
+ ``,
264
+ `### Converted Code`,
265
+ ``,
266
+ "```" + lang,
267
+ result.code,
268
+ "```",
269
+ ``,
270
+ ];
271
+ if (result.changes.length > 0) {
272
+ parts.push(`### Changes`);
273
+ result.changes.forEach((c, i) => parts.push(`${i + 1}. ${c}`));
274
+ parts.push(``);
275
+ }
276
+ if (result.warnings.length > 0) {
277
+ parts.push(`### Manual Attention Required`);
278
+ result.warnings.forEach((w, i) => parts.push(`${i + 1}. ${w}`));
279
+ }
280
+ return parts.join("\n");
281
+ }
282
+ export function formatMultiplatformResult(result) {
283
+ return [
284
+ `## Multiplatform Scene Code`,
285
+ `**Description:** "${result.description}"`,
286
+ ``,
287
+ `### Android (Kotlin / Jetpack Compose)`,
288
+ ``,
289
+ "```kotlin",
290
+ result.androidCode,
291
+ "```",
292
+ ``,
293
+ `### iOS (Swift / SwiftUI)`,
294
+ ``,
295
+ "```swift",
296
+ result.iosCode,
297
+ "```",
298
+ ``,
299
+ `### Notes`,
300
+ ...result.notes.map((n, i) => `${i + 1}. ${n}`),
301
+ ].join("\n");
302
+ }
@@ -292,7 +292,7 @@ fun DebugModelViewer() {
292
292
  title: "Build / Gradle Errors",
293
293
  guide: `## Debugging: Build Errors
294
294
 
295
- ### "Cannot resolve io.github.sceneview:sceneview:3.6.0"
295
+ ### "Cannot resolve io.github.sceneview:sceneview:3.6.2"
296
296
 
297
297
  1. Check repositories in \`settings.gradle.kts\`:
298
298
  \`\`\`kotlin
@@ -335,7 +335,7 @@ SceneView bundles Filament. If you also depend on Filament directly:
335
335
  \`\`\`kotlin
336
336
  // Remove direct Filament dependency — SceneView includes it
337
337
  // implementation("com.google.android.filament:filament-android:1.x.x") // REMOVE
338
- implementation("io.github.sceneview:sceneview:3.6.0") // This includes Filament
338
+ implementation("io.github.sceneview:sceneview:3.6.2") // This includes Filament
339
339
  \`\`\`
340
340
 
341
341
  ### "Cannot find Filament material"