sceneview-mcp 4.0.11 → 4.0.13

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.
@@ -604,4 +604,42 @@ export const TOOL_DEFINITIONS = [
604
604
  destructiveHint: false,
605
605
  },
606
606
  },
607
+ {
608
+ name: "search_android_docs",
609
+ description: "Searches Google's stock Android documentation knowledge base (~4 800 entries: Jetpack Compose, Camera2, ARCore SDK, Kotlin APIs, platform guides) and returns matching entries with their `kb://...` URIs. Use this to cross-reference stock Android APIs with SceneView code — e.g. \"how does LazyColumn paging work\", \"Camera2 capture session\", \"ARCore Config options\". Then call `fetch_android_doc` with a returned URI to read the full entry. Requires Google's `android` CLI on the MCP host's PATH (an optional runtime dependency — sceneview-mcp works without it); if the CLI is absent the tool returns clear install instructions instead of crashing.",
610
+ inputSchema: {
611
+ type: "object",
612
+ properties: {
613
+ query: {
614
+ type: "string",
615
+ description: "Free-text search query, e.g. \"LazyColumn paging\", \"Camera2 capture session\", \"ARCore anchors\".",
616
+ },
617
+ },
618
+ required: ["query"],
619
+ },
620
+ annotations: {
621
+ readOnlyHint: true,
622
+ openWorldHint: true,
623
+ destructiveHint: false,
624
+ },
625
+ },
626
+ {
627
+ name: "fetch_android_doc",
628
+ description: "Fetches the full text of a single stock Android documentation entry by its knowledge-base URI (`kb://...`), as returned by `search_android_docs`. Use this after `search_android_docs` to read a complete guide or API reference. Requires Google's `android` CLI on the MCP host's PATH (an optional runtime dependency); if the CLI is absent the tool returns clear install instructions instead of crashing.",
629
+ inputSchema: {
630
+ type: "object",
631
+ properties: {
632
+ uri: {
633
+ type: "string",
634
+ description: "The knowledge-base URI to fetch, e.g. \"kb://compose/lists/lazy-column\". A bare path with no scheme is tolerated and normalised to kb://.",
635
+ },
636
+ },
637
+ required: ["uri"],
638
+ },
639
+ annotations: {
640
+ readOnlyHint: true,
641
+ openWorldHint: true,
642
+ destructiveHint: false,
643
+ },
644
+ },
607
645
  ];
@@ -34,7 +34,9 @@ import { ANIMATION_GUIDE, GESTURE_GUIDE, PERFORMANCE_TIPS, } from "../advanced-g
34
34
  import { MATERIAL_GUIDE, COLLISION_GUIDE, MODEL_OPTIMIZATION_GUIDE, WEB_RENDERING_GUIDE, } from "../extra-guides.js";
35
35
  import { searchModels, formatSearchResults } from "../search-models.js";
36
36
  import { analyzeProject, formatAnalysisReport } from "../analyze-project.js";
37
+ import { searchAndroidDocs, fetchAndroidDoc, formatAndroidDocsSearch, formatAndroidDocsFetch, } from "../android-docs.js";
37
38
  import { LLMS_TXT } from "../generated/llms-txt.js";
39
+ import { LATEST_SCENEVIEW_RELEASE } from "../generated/version.js";
38
40
  // ─── Legal disclaimer (identical to index.ts 4.0.0) ─────────────────────
39
41
  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).*";
40
42
  // ─── Sponsor CTA (shown every N tool calls, opt-out via env var) ────────────
@@ -111,7 +113,7 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
111
113
  ? [
112
114
  `**SPM dependency:**`,
113
115
  `\`\`\`swift`,
114
- `.package(url: "${sample.spmDependency ?? sample.dependency}", from: "4.0.0")`,
116
+ `.package(url: "${sample.spmDependency ?? sample.dependency}", from: "${LATEST_SCENEVIEW_RELEASE}")`,
115
117
  `\`\`\``,
116
118
  ]
117
119
  : [
@@ -184,7 +186,7 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
184
186
  `### build.gradle.kts`,
185
187
  `\`\`\`kotlin`,
186
188
  `dependencies {`,
187
- ` implementation("io.github.sceneview:sceneview:4.0.0")`,
189
+ ` implementation("io.github.sceneview:sceneview:${LATEST_SCENEVIEW_RELEASE}")`,
188
190
  `}`,
189
191
  `\`\`\``,
190
192
  ``,
@@ -205,7 +207,7 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
205
207
  `### build.gradle.kts`,
206
208
  `\`\`\`kotlin`,
207
209
  `dependencies {`,
208
- ` implementation("io.github.sceneview:arsceneview:4.0.0")`,
210
+ ` implementation("io.github.sceneview:arsceneview:${LATEST_SCENEVIEW_RELEASE}")`,
209
211
  `}`,
210
212
  `\`\`\``,
211
213
  ``,
@@ -318,7 +320,7 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
318
320
  `\`\`\``,
319
321
  `https://github.com/sceneview/sceneview`,
320
322
  `\`\`\``,
321
- `Set version rule to **"from: 4.0.0"**.`,
323
+ `Set version rule to **"from: ${LATEST_SCENEVIEW_RELEASE}"**.`,
322
324
  ``,
323
325
  `Or in Package.swift:`,
324
326
  `\`\`\`swift`,
@@ -329,7 +331,7 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
329
331
  ` name: "MyApp",`,
330
332
  ` platforms: [.iOS(.v17), .macOS(.v14), .visionOS(.v1)],`,
331
333
  ` dependencies: [`,
332
- ` .package(url: "https://github.com/sceneview/sceneview", from: "4.0.0")`,
334
+ ` .package(url: "https://github.com/sceneview/sceneview", from: "${LATEST_SCENEVIEW_RELEASE}")`,
333
335
  ` ],`,
334
336
  ` targets: [`,
335
337
  ` .executableTarget(`,
@@ -400,7 +402,7 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
400
402
  `### 1. Add SPM Dependency`,
401
403
  ``,
402
404
  `\`\`\`swift`,
403
- `.package(url: "https://github.com/sceneview/sceneview", from: "4.0.0")`,
405
+ `.package(url: "https://github.com/sceneview/sceneview", from: "${LATEST_SCENEVIEW_RELEASE}")`,
404
406
  `\`\`\``,
405
407
  ``,
406
408
  `### 2. Minimum Platform`,
@@ -679,16 +681,22 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
679
681
  }
680
682
  // ── list_platforms ────────────────────────────────────────────────────────
681
683
  case "list_platforms": {
684
+ // All `version:` and `dependency:` fields use `LATEST_SCENEVIEW_RELEASE`
685
+ // explicitly so each platform row renders the actually-current SDK
686
+ // version. Pre-fix these were double-quoted strings with a literal
687
+ // `${LATEST_SCENEVIEW_RELEASE}` substring that never interpolated —
688
+ // users following list_platforms output saw the unresolved template
689
+ // text. Backticks throughout = real interpolation. See #941 follow-up.
682
690
  const platforms = [
683
- { platform: "Android", renderer: "Filament", framework: "Jetpack Compose", status: "Stable", version: "4.0.0", dependency: "io.github.sceneview:sceneview:4.0.0", features: ["3D", "AR (ARCore)", "Model loading (GLB/glTF)", "Geometry nodes", "Physics", "Gestures"] },
684
- { platform: "Android TV", renderer: "Filament", framework: "Compose TV", status: "Alpha", version: "4.0.0", dependency: "io.github.sceneview:sceneview:4.0.0", features: ["3D", "D-pad controls", "Auto-rotation", "Model loading"] },
691
+ { platform: "Android", renderer: "Filament", framework: "Jetpack Compose", status: "Stable", version: LATEST_SCENEVIEW_RELEASE, dependency: `io.github.sceneview:sceneview:${LATEST_SCENEVIEW_RELEASE}`, features: ["3D", "AR (ARCore)", "Model loading (GLB/glTF)", "Geometry nodes", "Physics", "Gestures"] },
692
+ { platform: "Android TV", renderer: "Filament", framework: "Compose TV", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: `io.github.sceneview:sceneview:${LATEST_SCENEVIEW_RELEASE}`, features: ["3D", "D-pad controls", "Auto-rotation", "Model loading"] },
685
693
  { platform: "Android XR", renderer: "Jetpack XR SceneCore", framework: "Compose XR", status: "Planned", version: "-", dependency: "-", features: ["Spatial computing", "Hand tracking", "Passthrough"] },
686
- { platform: "iOS", renderer: "RealityKit", framework: "SwiftUI", status: "Alpha", version: "4.0.0", dependency: "SceneViewSwift (SPM)", features: ["3D", "AR (ARKit)", "16 node types", "USDZ models"] },
687
- { platform: "macOS", renderer: "RealityKit", framework: "SwiftUI", status: "Alpha", version: "4.0.0", dependency: "SceneViewSwift (SPM)", features: ["3D", "Orbit camera", "USDZ models"] },
688
- { platform: "visionOS", renderer: "RealityKit", framework: "SwiftUI", status: "Alpha", version: "4.0.0", dependency: "SceneViewSwift (SPM)", features: ["3D", "Immersive spaces", "Hand tracking (planned)"] },
689
- { platform: "Web", renderer: "Filament.js (WASM)", framework: "Kotlin/JS", status: "Alpha", version: "4.0.0", dependency: "@sceneview/sceneview-web", features: ["3D", "WebXR AR/VR", "GLB models", "WebGL2"] },
690
- { platform: "Desktop", renderer: "Software / Filament JNI", framework: "Compose Desktop", status: "Alpha", version: "4.0.0", dependency: "sceneview-desktop (local)", features: ["3D", "Software renderer", "Wireframe"] },
691
- { platform: "Flutter", renderer: "Filament / RealityKit", framework: "PlatformView", status: "Alpha", version: "4.0.0", dependency: "flutter pub: sceneview", features: ["3D", "AR", "Android + iOS bridge"] },
694
+ { platform: "iOS", renderer: "RealityKit", framework: "SwiftUI", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "SceneViewSwift (SPM)", features: ["3D", "AR (ARKit)", "16 node types", "USDZ models"] },
695
+ { platform: "macOS", renderer: "RealityKit", framework: "SwiftUI", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "SceneViewSwift (SPM)", features: ["3D", "Orbit camera", "USDZ models"] },
696
+ { platform: "visionOS", renderer: "RealityKit", framework: "SwiftUI", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "SceneViewSwift (SPM)", features: ["3D", "Immersive spaces", "Hand tracking (planned)"] },
697
+ { platform: "Web", renderer: "Filament.js (WASM)", framework: "Kotlin/JS", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "sceneview-web (npm)", features: ["3D", "WebXR AR/VR", "GLB models", "WebGL2"] },
698
+ { platform: "Desktop", renderer: "Software / Filament JNI", framework: "Compose Desktop", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "sceneview-desktop (local)", features: ["3D", "Software renderer", "Wireframe"] },
699
+ { platform: "Flutter", renderer: "Filament / RealityKit", framework: "PlatformView", status: "Alpha", version: LATEST_SCENEVIEW_RELEASE, dependency: "flutter pub: sceneview_flutter (git ref)", features: ["3D", "AR", "Android + iOS bridge"] },
692
700
  ];
693
701
  const lines = [
694
702
  "## SceneView Supported Platforms\n",
@@ -782,6 +790,38 @@ export async function dispatchTool(toolName, args, _ctx = {}) {
782
790
  };
783
791
  }
784
792
  }
793
+ // ── search_android_docs ──────────────────────────────────────────────────
794
+ case "search_android_docs": {
795
+ const query = args?.query;
796
+ if (!query || typeof query !== "string" || query.trim().length === 0) {
797
+ return {
798
+ content: [{ type: "text", text: "Missing required parameter: `query` must be a non-empty string." }],
799
+ isError: true,
800
+ };
801
+ }
802
+ const docsResult = await searchAndroidDocs(query);
803
+ const text = formatAndroidDocsSearch(query, docsResult);
804
+ return {
805
+ content: withDisclaimer([{ type: "text", text }]),
806
+ isError: docsResult.ok ? undefined : true,
807
+ };
808
+ }
809
+ // ── fetch_android_doc ────────────────────────────────────────────────────
810
+ case "fetch_android_doc": {
811
+ const uri = args?.uri;
812
+ if (!uri || typeof uri !== "string" || uri.trim().length === 0) {
813
+ return {
814
+ content: [{ type: "text", text: "Missing required parameter: `uri` must be a non-empty `kb://...` string." }],
815
+ isError: true,
816
+ };
817
+ }
818
+ const docResult = await fetchAndroidDoc(uri);
819
+ const text = formatAndroidDocsFetch(uri, docResult);
820
+ return {
821
+ content: withDisclaimer([{ type: "text", text }]),
822
+ isError: docResult.ok ? undefined : true,
823
+ };
824
+ }
785
825
  default:
786
826
  return {
787
827
  content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
package/dist/validator.js CHANGED
@@ -180,6 +180,15 @@ const RULES = [
180
180
  check(code, lines) {
181
181
  const issues = [];
182
182
  const renames = [
183
+ // The headline 4.0 rename — `Scene { }` / `ARScene { }` became
184
+ // `SceneView { }` / `ARSceneView { }`. Pre-#939 this was missing
185
+ // from the validator so legacy snippets passed as "no issues".
186
+ // The regex requires a word boundary + the opening `{` of the
187
+ // trailing-lambda call, so we don't false-positive on Filament's
188
+ // own `Scene` class (which is referenced with `()` or `.`, never
189
+ // followed by `{`).
190
+ [/\bScene\s*\{/, "`Scene { }` → renamed to `SceneView { }` in 4.0"],
191
+ [/\bARScene\s*\{/, "`ARScene { }` → renamed to `ARSceneView { }` in 4.0"],
183
192
  [/\bArSceneView\s*\(/, "`ArSceneView(…)` → renamed to `ARSceneView(…)` in 3.0"],
184
193
  [/\bPlacementNode\b/, "`PlacementNode` removed → use `AnchorNode` + `HitResultNode` in 3.0"],
185
194
  [/\bTransformableNode\b/, "`TransformableNode` removed → set `isEditable = true` on `ModelNode` in 3.0"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sceneview-mcp",
3
- "version": "4.0.11",
3
+ "version": "4.0.13",
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,7 +44,15 @@
44
44
  "dist/**/*.js",
45
45
  "!dist/**/*.test.js",
46
46
  "!dist/**/__fixtures__/**",
47
- "llms.txt",
47
+ "!dist/auth.js",
48
+ "!dist/billing.js",
49
+ "!dist/convert-platform.js",
50
+ "!dist/optimize-scene.js",
51
+ "!dist/explain-api.js",
52
+ "!dist/generate-animation.js",
53
+ "!dist/generate-environment.js",
54
+ "!dist/generate-gesture.js",
55
+ "!dist/generate-physics.js",
48
56
  "README.md"
49
57
  ],
50
58
  "engines": {
@@ -53,15 +61,18 @@
53
61
  "scripts": {
54
62
  "prebuild": "node scripts/generate-llms-txt.js && node scripts/generate-version.js",
55
63
  "build": "tsc",
56
- "prepare": "cp ../llms.txt ./llms.txt && node scripts/generate-llms-txt.js && node scripts/generate-version.js && tsc",
64
+ "prepare": "node scripts/generate-llms-txt.js && node scripts/generate-version.js && tsc",
57
65
  "start": "node dist/index.js",
58
66
  "dev": "tsx src/index.ts",
59
- "test": "node scripts/generate-llms-txt.js && node scripts/generate-version.js && vitest run"
67
+ "test": "node scripts/generate-llms-txt.js && node scripts/generate-version.js && vitest run",
68
+ "biome": "cd .. && mcp/node_modules/.bin/biome check",
69
+ "biome:fix": "cd .. && mcp/node_modules/.bin/biome check --write"
60
70
  },
61
71
  "dependencies": {
62
72
  "@modelcontextprotocol/sdk": "^1.29.0"
63
73
  },
64
74
  "devDependencies": {
75
+ "@biomejs/biome": "^2.0.0",
65
76
  "@types/node": "^25.5.0",
66
77
  "tsx": "^4.0.0",
67
78
  "typescript": "^6.0.2",
package/dist/auth.js DELETED
@@ -1,84 +0,0 @@
1
- /**
2
- * Authentication middleware for the SceneView MCP server.
3
- *
4
- * Provides tool-level access control based on free/pro tiers,
5
- * API key validation, and MCP-formatted denial responses.
6
- */
7
- import { isProTool, PRO_UPGRADE_MESSAGE } from "./tiers.js";
8
- import { getConfiguredApiKey, validateApiKey } from "./billing.js";
9
- // ─── Main middleware ─────────────────────────────────────────────────────────
10
- /**
11
- * Checks whether the current user is allowed to invoke the given tool.
12
- *
13
- * - Free-tier tools are always allowed.
14
- * - Pro-tier tools require a valid API key with an active subscription.
15
- */
16
- export async function checkToolAccess(toolName) {
17
- // Free-tier tools are always accessible
18
- if (!isProTool(toolName)) {
19
- return { allowed: true, tier: "free" };
20
- }
21
- // Pro tool — check for API key
22
- const apiKey = getConfiguredApiKey();
23
- if (!apiKey) {
24
- return {
25
- allowed: false,
26
- tier: "free",
27
- message: PRO_UPGRADE_MESSAGE,
28
- };
29
- }
30
- // Validate the key against the billing service
31
- const validation = await validateApiKey(apiKey);
32
- if (validation.valid) {
33
- return { allowed: true, tier: "pro" };
34
- }
35
- // Key exists but is invalid or subscription expired
36
- return {
37
- allowed: false,
38
- tier: "free",
39
- message: validation.error ?? "Your API key is invalid or your Pro subscription has expired.",
40
- };
41
- }
42
- // ─── Tool list filtering ─────────────────────────────────────────────────────
43
- /**
44
- * Annotates the tool list based on the caller's tier.
45
- *
46
- * - Free users see every tool, but pro-only tools get a "[PRO]" prefix on
47
- * their description so the AI (and the human) know an upgrade is needed.
48
- * - Pro users see the list unmodified.
49
- */
50
- export async function filterToolsForTier(tools) {
51
- const apiKey = getConfiguredApiKey();
52
- let isPro = false;
53
- if (apiKey) {
54
- const validation = await validateApiKey(apiKey);
55
- isPro = validation.valid;
56
- }
57
- if (isPro) {
58
- return tools;
59
- }
60
- // Free tier — prefix pro tool descriptions so users can discover them
61
- return tools.map((tool) => {
62
- if (!isProTool(tool.name)) {
63
- return tool;
64
- }
65
- const description = typeof tool.description === "string" ? tool.description : "";
66
- return {
67
- ...tool,
68
- description: `[PRO] ${description}`,
69
- };
70
- });
71
- }
72
- // ─── MCP response helpers ────────────────────────────────────────────────────
73
- /**
74
- * Builds an MCP-formatted error response for access-denied scenarios.
75
- *
76
- * The returned object can be used directly as the handler return value
77
- * for a `CallToolRequestSchema` handler.
78
- */
79
- export function createAccessDeniedResponse(toolName, message) {
80
- return {
81
- content: [{ type: "text", text: message }],
82
- isError: true,
83
- };
84
- }
package/dist/billing.js DELETED
@@ -1,137 +0,0 @@
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
- }