sceneview-mcp 3.0.0 → 3.0.2
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/README.md +64 -107
- package/dist/index.js +113 -8
- package/dist/issues.js +72 -0
- package/dist/issues.test.js +114 -0
- package/dist/migration.js +248 -0
- package/dist/migration.test.js +50 -0
- package/dist/samples.js +61 -0
- package/dist/samples.test.js +119 -0
- package/dist/validator.js +251 -0
- package/dist/validator.test.js +246 -0
- package/package.json +32 -5
package/README.md
CHANGED
|
@@ -1,36 +1,17 @@
|
|
|
1
1
|
# sceneview-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/sceneview-mcp)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
MCP server for [SceneView](https://github.com/SceneView/sceneview-android) — 3D and AR as Jetpack Compose composables for Android.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
## What it provides
|
|
10
|
-
|
|
11
|
-
**Resource — `sceneview://api`**
|
|
12
|
-
The complete SceneView 3.0.0 API reference (llms.txt): composable signatures, node types, AR scope, resource loading, threading rules, common patterns.
|
|
13
|
-
|
|
14
|
-
**Tool — `get_sample`**
|
|
15
|
-
|
|
16
|
-
| Scenario | What you get |
|
|
17
|
-
|---|---|
|
|
18
|
-
| `model-viewer` | Full-screen 3D scene, HDR environment, orbit camera |
|
|
19
|
-
| `ar-tap-to-place` | AR tap-to-place with pinch-to-scale and drag-to-rotate |
|
|
20
|
-
| `ar-placement-cursor` | AR reticle that snaps to surfaces, tap to confirm |
|
|
21
|
-
| `ar-augmented-image` | Detect a reference image, overlay a 3D model |
|
|
22
|
-
| `ar-face-filter` | Front-camera face mesh with a custom material |
|
|
23
|
-
|
|
24
|
-
**Tool — `get_setup`**
|
|
25
|
-
Gradle dependency + AndroidManifest for `"3d"` or `"ar"` projects.
|
|
8
|
+
Add this to Claude and it **always knows how to use SceneView**. No copy-pasting docs. No hallucinated APIs. Correct, compilable Kotlin — first try.
|
|
26
9
|
|
|
27
10
|
---
|
|
28
11
|
|
|
29
|
-
##
|
|
12
|
+
## Quick start
|
|
30
13
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Add `.claude/mcp.json` at your Android project root:
|
|
14
|
+
Add to your Claude config and you're done:
|
|
34
15
|
|
|
35
16
|
```json
|
|
36
17
|
{
|
|
@@ -43,112 +24,80 @@ Add `.claude/mcp.json` at your Android project root:
|
|
|
43
24
|
}
|
|
44
25
|
```
|
|
45
26
|
|
|
46
|
-
|
|
27
|
+
<details>
|
|
28
|
+
<summary>Where does this go?</summary>
|
|
47
29
|
|
|
48
|
-
|
|
30
|
+
| Client | Config file |
|
|
31
|
+
|---|---|
|
|
32
|
+
| **Claude Code** (project) | `.claude/mcp.json` at project root |
|
|
33
|
+
| **Claude Code** (global) | `~/.claude/mcp.json` |
|
|
34
|
+
| **Claude Desktop** | `claude_desktop_config.json` |
|
|
49
35
|
|
|
50
|
-
|
|
36
|
+
After saving, run `/mcp` in Claude Code or restart Claude Desktop to pick it up.
|
|
37
|
+
</details>
|
|
51
38
|
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
"mcpServers": {
|
|
55
|
-
"sceneview": {
|
|
56
|
-
"command": "npx",
|
|
57
|
-
"args": ["-y", "sceneview-mcp"]
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
```
|
|
39
|
+
---
|
|
62
40
|
|
|
63
|
-
|
|
41
|
+
## What Claude gets
|
|
64
42
|
|
|
65
|
-
|
|
43
|
+
### Resource — `sceneview://api`
|
|
66
44
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
45
|
+
The complete SceneView 3.0.0 API reference (`llms.txt`): composable signatures, node types, AR scope, resource loading, threading rules, and common patterns.
|
|
46
|
+
|
|
47
|
+
### Tool — `get_sample(scenario)`
|
|
48
|
+
|
|
49
|
+
Returns a complete, compilable Kotlin sample.
|
|
50
|
+
|
|
51
|
+
| Scenario | What you get |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `model-viewer` | Full-screen 3D scene, HDR environment, orbit camera |
|
|
54
|
+
| `ar-tap-to-place` | AR tap-to-place with pinch-to-scale and drag-to-rotate |
|
|
55
|
+
| `ar-placement-cursor` | AR reticle that snaps to surfaces, tap to confirm |
|
|
56
|
+
| `ar-augmented-image` | Detect a reference image, overlay a 3D model |
|
|
57
|
+
| `ar-face-filter` | Front-camera face mesh with a custom material |
|
|
58
|
+
|
|
59
|
+
### Tool — `get_setup(type)`
|
|
60
|
+
|
|
61
|
+
Returns Gradle dependencies + AndroidManifest for `"3d"` or `"ar"` projects.
|
|
77
62
|
|
|
78
63
|
---
|
|
79
64
|
|
|
80
65
|
## How it works
|
|
81
66
|
|
|
82
67
|
```
|
|
83
|
-
|
|
68
|
+
You: "Add AR placement to my app"
|
|
84
69
|
│
|
|
85
|
-
|
|
86
|
-
Claude reads sceneview://api ← full llms.txt, always current
|
|
70
|
+
Claude: reads sceneview://api ← full API ref, always current
|
|
87
71
|
│
|
|
88
|
-
|
|
89
|
-
Claude calls get_sample("ar-tap-to-place") ← working Kotlin boilerplate
|
|
72
|
+
Claude: calls get_sample("ar-tap-to-place") ← working Kotlin
|
|
90
73
|
│
|
|
91
|
-
|
|
92
|
-
Correct, compilable SceneView 3.0.0 code — first try, zero hallucination
|
|
74
|
+
Result: Correct, compilable SceneView 3.0.0 code
|
|
93
75
|
```
|
|
94
76
|
|
|
95
77
|
---
|
|
96
78
|
|
|
97
|
-
##
|
|
79
|
+
## Try it — sample prompts
|
|
98
80
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Create an Android Compose screen called ModelViewerScreen that loads
|
|
102
|
-
assets/models/my_model.glb in a full-screen 3D scene with orbit camera and HDR
|
|
103
|
-
environment from assets/environments/sky_2k.hdr.
|
|
104
|
-
Use SceneView io.github.sceneview:sceneview:3.0.0.
|
|
105
|
-
```
|
|
81
|
+
**3D model viewer**
|
|
82
|
+
> Create a Compose screen that loads `models/helmet.glb` in a full-screen 3D scene with orbit camera and HDR environment. Use SceneView 3.0.0.
|
|
106
83
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
Create an Android Compose AR screen called TapToPlaceScreen. Show a plane
|
|
110
|
-
detection grid. Tapping places assets/models/chair.glb on the surface with
|
|
111
|
-
pinch-to-scale and drag-to-rotate. Multiple taps = multiple objects.
|
|
112
|
-
Use SceneView io.github.sceneview:arsceneview:3.0.0.
|
|
113
|
-
```
|
|
84
|
+
**AR tap-to-place**
|
|
85
|
+
> Create an AR Compose screen. Tapping a detected surface places `models/chair.glb` with pinch-to-scale and drag-to-rotate. Multiple taps = multiple objects.
|
|
114
86
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
Create an AR screen called ARCursorScreen with a reticle that snaps to surfaces
|
|
118
|
-
at screen center. Tap to place assets/models/object.glb and hide the reticle.
|
|
119
|
-
Use SceneView io.github.sceneview:arsceneview:3.0.0.
|
|
120
|
-
```
|
|
87
|
+
**AR placement cursor**
|
|
88
|
+
> Create an AR screen with a reticle that snaps to surfaces at screen center. Tap to place `models/object.glb` and hide the reticle.
|
|
121
89
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
Create an AR screen called AugmentedImageScreen that detects R.drawable.target_image
|
|
125
|
-
(15 cm wide) and places assets/models/overlay.glb above it scaled to image width.
|
|
126
|
-
Model disappears when tracking is lost.
|
|
127
|
-
Use SceneView io.github.sceneview:arsceneview:3.0.0.
|
|
128
|
-
```
|
|
90
|
+
**AR augmented image**
|
|
91
|
+
> Create an AR screen that detects `R.drawable.target_image` (15 cm) and places `models/overlay.glb` above it, scaled to match.
|
|
129
92
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
Create an AR screen called FaceFilterScreen using the front camera that detects
|
|
133
|
-
faces and applies assets/materials/face_mask.filamat to the face mesh.
|
|
134
|
-
Use SceneView io.github.sceneview:arsceneview:3.0.0.
|
|
135
|
-
```
|
|
93
|
+
**AR face filter**
|
|
94
|
+
> Create an AR screen using the front camera that detects faces and applies `materials/face_mask.filamat` to the mesh.
|
|
136
95
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
Create a 3D product configurator screen with Red/Blue/Green color buttons.
|
|
140
|
-
Apply the selected color as a solid material on assets/models/product.glb.
|
|
141
|
-
Add orbit camera and pinch-to-zoom.
|
|
142
|
-
Use SceneView io.github.sceneview:sceneview:3.0.0.
|
|
143
|
-
```
|
|
96
|
+
**Product configurator**
|
|
97
|
+
> Create a 3D product configurator with Red/Blue/Green buttons. Apply the color as a material on `models/product.glb`. Add orbit camera and pinch-to-zoom.
|
|
144
98
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
Create an AR screen where a bottom sheet lets users choose between chair, table,
|
|
148
|
-
and lamp GLBs in assets/models/. Tapping places the selected model. Each object
|
|
149
|
-
is independently pinch-to-scale and drag-to-rotate. A "Clear all" button removes
|
|
150
|
-
everything. Use SceneView io.github.sceneview:arsceneview:3.0.0.
|
|
151
|
-
```
|
|
99
|
+
**AR multi-object scene**
|
|
100
|
+
> Create an AR screen where a bottom sheet lets users pick between chair, table, and lamp GLBs. Tap to place. Each object gets pinch-to-scale and drag-to-rotate.
|
|
152
101
|
|
|
153
102
|
---
|
|
154
103
|
|
|
@@ -157,9 +106,9 @@ everything. Use SceneView io.github.sceneview:arsceneview:3.0.0.
|
|
|
157
106
|
```bash
|
|
158
107
|
cd mcp
|
|
159
108
|
npm install
|
|
160
|
-
npm run prepare # copies
|
|
109
|
+
npm run prepare # copies llms.txt + compiles TypeScript
|
|
161
110
|
npm start # run over stdio
|
|
162
|
-
npx @modelcontextprotocol/inspector node dist/index.js
|
|
111
|
+
npx @modelcontextprotocol/inspector node dist/index.js # test in MCP inspector
|
|
163
112
|
```
|
|
164
113
|
|
|
165
114
|
## Publishing
|
|
@@ -169,3 +118,11 @@ cd mcp
|
|
|
169
118
|
npm run prepare
|
|
170
119
|
npm publish --access public
|
|
171
120
|
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Links
|
|
125
|
+
|
|
126
|
+
- **SDK**: [github.com/SceneView/sceneview-android](https://github.com/SceneView/sceneview-android)
|
|
127
|
+
- **npm**: [npmjs.com/package/sceneview-mcp](https://www.npmjs.com/package/sceneview-mcp)
|
|
128
|
+
- **MCP spec**: [modelcontextprotocol.io](https://modelcontextprotocol.io)
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,9 @@ import { readFileSync } from "fs";
|
|
|
6
6
|
import { dirname, resolve } from "path";
|
|
7
7
|
import { fileURLToPath } from "url";
|
|
8
8
|
import { getSample, SAMPLE_IDS, SAMPLES } from "./samples.js";
|
|
9
|
+
import { validateCode, formatValidationReport } from "./validator.js";
|
|
10
|
+
import { MIGRATION_GUIDE } from "./migration.js";
|
|
11
|
+
import { fetchKnownIssues } from "./issues.js";
|
|
9
12
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
13
|
let API_DOCS;
|
|
11
14
|
try {
|
|
@@ -14,7 +17,8 @@ try {
|
|
|
14
17
|
catch {
|
|
15
18
|
API_DOCS = "SceneView API docs not found. Run `npm run prepare` to bundle llms.txt.";
|
|
16
19
|
}
|
|
17
|
-
const server = new Server({ name: "@sceneview/mcp", version: "3.0.
|
|
20
|
+
const server = new Server({ name: "@sceneview/mcp", version: "3.0.2" }, { capabilities: { resources: {}, tools: {} } });
|
|
21
|
+
// ─── Resources ───────────────────────────────────────────────────────────────
|
|
18
22
|
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
19
23
|
resources: [
|
|
20
24
|
{
|
|
@@ -23,21 +27,36 @@ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
|
23
27
|
description: "Complete SceneView 3.0.0 API — Scene, ARScene, SceneScope DSL, ARSceneScope DSL, node types, resource loading, camera, gestures, math types, threading rules, and common patterns. Read this before writing any SceneView code.",
|
|
24
28
|
mimeType: "text/markdown",
|
|
25
29
|
},
|
|
30
|
+
{
|
|
31
|
+
uri: "sceneview://known-issues",
|
|
32
|
+
name: "SceneView Open GitHub Issues",
|
|
33
|
+
description: "Live list of open issues from the SceneView GitHub repository. Check this before reporting a bug or when something isn't working — there may already be a known workaround.",
|
|
34
|
+
mimeType: "text/markdown",
|
|
35
|
+
},
|
|
26
36
|
],
|
|
27
37
|
}));
|
|
28
38
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
switch (request.params.uri) {
|
|
40
|
+
case "sceneview://api":
|
|
41
|
+
return {
|
|
42
|
+
contents: [{ uri: "sceneview://api", mimeType: "text/markdown", text: API_DOCS }],
|
|
43
|
+
};
|
|
44
|
+
case "sceneview://known-issues": {
|
|
45
|
+
const issues = await fetchKnownIssues();
|
|
46
|
+
return {
|
|
47
|
+
contents: [{ uri: "sceneview://known-issues", mimeType: "text/markdown", text: issues }],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`Unknown resource: ${request.params.uri}`);
|
|
33
52
|
}
|
|
34
|
-
throw new Error(`Unknown resource: ${request.params.uri}`);
|
|
35
53
|
});
|
|
54
|
+
// ─── Tools ───────────────────────────────────────────────────────────────────
|
|
36
55
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
37
56
|
tools: [
|
|
38
57
|
{
|
|
39
58
|
name: "get_sample",
|
|
40
|
-
description: "Returns a complete, compilable Kotlin sample for a given SceneView scenario. Use this to get a working starting point before customising.",
|
|
59
|
+
description: "Returns a complete, compilable Kotlin sample for a given SceneView scenario. Use this to get a working starting point before customising. Call `list_samples` first if you are unsure which scenario fits.",
|
|
41
60
|
inputSchema: {
|
|
42
61
|
type: "object",
|
|
43
62
|
properties: {
|
|
@@ -50,6 +69,20 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
50
69
|
required: ["scenario"],
|
|
51
70
|
},
|
|
52
71
|
},
|
|
72
|
+
{
|
|
73
|
+
name: "list_samples",
|
|
74
|
+
description: "Lists all available SceneView code samples with their IDs, descriptions, and tags. Use this to find the right sample before calling `get_sample`, or to show the user what SceneView can do.",
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
properties: {
|
|
78
|
+
tag: {
|
|
79
|
+
type: "string",
|
|
80
|
+
description: "Optional tag to filter by (e.g. \"ar\", \"3d\", \"anchor\", \"face-tracking\", \"geometry\", \"animation\"). Omit to list all samples.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
required: [],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
53
86
|
{
|
|
54
87
|
name: "get_setup",
|
|
55
88
|
description: "Returns the Gradle dependency and AndroidManifest snippet required to use SceneView in an Android project.",
|
|
@@ -65,16 +98,46 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
65
98
|
required: ["type"],
|
|
66
99
|
},
|
|
67
100
|
},
|
|
101
|
+
{
|
|
102
|
+
name: "validate_code",
|
|
103
|
+
description: "Checks a Kotlin SceneView snippet for common mistakes: threading violations, wrong destroy order, missing null-checks on rememberModelInstance, LightNode trailing-lambda bug, deprecated 2.x APIs, and more. Always call this before presenting generated SceneView code to the user.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
code: {
|
|
108
|
+
type: "string",
|
|
109
|
+
description: "The Kotlin source code to validate (composable function, class, or file).",
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
required: ["code"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "get_migration_guide",
|
|
117
|
+
description: "Returns the full SceneView 2.x → 3.0 migration guide. Use this when a user reports code that worked in 2.x but breaks in 3.0, or when helping someone upgrade.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {},
|
|
121
|
+
required: [],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
68
124
|
],
|
|
69
125
|
}));
|
|
126
|
+
// ─── Tool handlers ────────────────────────────────────────────────────────────
|
|
70
127
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
71
128
|
switch (request.params.name) {
|
|
129
|
+
// ── get_sample ────────────────────────────────────────────────────────────
|
|
72
130
|
case "get_sample": {
|
|
73
131
|
const scenario = request.params.arguments?.scenario;
|
|
74
132
|
const sample = getSample(scenario);
|
|
75
133
|
if (!sample) {
|
|
76
134
|
return {
|
|
77
|
-
content: [
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: `Unknown scenario "${scenario}". Call \`list_samples\` to see available options.`,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
78
141
|
isError: true,
|
|
79
142
|
};
|
|
80
143
|
}
|
|
@@ -85,6 +148,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
85
148
|
text: [
|
|
86
149
|
`## ${sample.title}`,
|
|
87
150
|
``,
|
|
151
|
+
`**Tags:** ${sample.tags.join(", ")}`,
|
|
152
|
+
``,
|
|
88
153
|
`**Gradle dependency:**`,
|
|
89
154
|
`\`\`\`kotlin`,
|
|
90
155
|
`implementation("${sample.dependency}")`,
|
|
@@ -102,6 +167,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
102
167
|
],
|
|
103
168
|
};
|
|
104
169
|
}
|
|
170
|
+
// ── list_samples ──────────────────────────────────────────────────────────
|
|
171
|
+
case "list_samples": {
|
|
172
|
+
const filterTag = request.params.arguments?.tag;
|
|
173
|
+
const entries = Object.values(SAMPLES).filter((s) => !filterTag || s.tags.includes(filterTag));
|
|
174
|
+
if (entries.length === 0) {
|
|
175
|
+
return {
|
|
176
|
+
content: [
|
|
177
|
+
{
|
|
178
|
+
type: "text",
|
|
179
|
+
text: `No samples found with tag "${filterTag}". Available tags: 3d, ar, model, geometry, animation, camera, environment, anchor, plane-detection, image-tracking, face-tracking, placement, gestures`,
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const header = filterTag
|
|
185
|
+
? `## SceneView samples tagged \`${filterTag}\` (${entries.length})\n`
|
|
186
|
+
: `## All SceneView samples (${entries.length})\n`;
|
|
187
|
+
const rows = entries
|
|
188
|
+
.map((s) => `### \`${s.id}\`\n**${s.title}**\n${s.description}\n*Tags:* ${s.tags.join(", ")}\n*Dependency:* \`${s.dependency}\`\n\nCall \`get_sample("${s.id}")\` for the full code.`)
|
|
189
|
+
.join("\n\n---\n\n");
|
|
190
|
+
return { content: [{ type: "text", text: header + rows }] };
|
|
191
|
+
}
|
|
192
|
+
// ── get_setup ─────────────────────────────────────────────────────────────
|
|
105
193
|
case "get_setup": {
|
|
106
194
|
const type = request.params.arguments?.type;
|
|
107
195
|
if (type === "3d") {
|
|
@@ -158,6 +246,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
158
246
|
isError: true,
|
|
159
247
|
};
|
|
160
248
|
}
|
|
249
|
+
// ── validate_code ─────────────────────────────────────────────────────────
|
|
250
|
+
case "validate_code": {
|
|
251
|
+
const code = request.params.arguments?.code;
|
|
252
|
+
if (!code || typeof code !== "string") {
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: "text", text: "Missing required parameter: `code`" }],
|
|
255
|
+
isError: true,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const issues = validateCode(code);
|
|
259
|
+
const report = formatValidationReport(issues);
|
|
260
|
+
return { content: [{ type: "text", text: report }] };
|
|
261
|
+
}
|
|
262
|
+
// ── get_migration_guide ───────────────────────────────────────────────────
|
|
263
|
+
case "get_migration_guide": {
|
|
264
|
+
return { content: [{ type: "text", text: MIGRATION_GUIDE }] };
|
|
265
|
+
}
|
|
161
266
|
default:
|
|
162
267
|
return {
|
|
163
268
|
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
package/dist/issues.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Cache for 10 minutes — avoids hammering the GitHub API on every tool call
|
|
2
|
+
const CACHE_TTL_MS = 10 * 60 * 1000;
|
|
3
|
+
let cache = null;
|
|
4
|
+
export async function fetchKnownIssues() {
|
|
5
|
+
const now = Date.now();
|
|
6
|
+
if (cache && now - cache.fetchedAt < CACHE_TTL_MS) {
|
|
7
|
+
return cache.data;
|
|
8
|
+
}
|
|
9
|
+
let issues = [];
|
|
10
|
+
let fetchError = null;
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch("https://api.github.com/repos/SceneView/sceneview-android/issues?state=open&per_page=30", {
|
|
13
|
+
headers: {
|
|
14
|
+
Accept: "application/vnd.github+json",
|
|
15
|
+
"User-Agent": "sceneview-mcp/3.0",
|
|
16
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok) {
|
|
20
|
+
fetchError = `GitHub API returned ${response.status}: ${response.statusText}`;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
issues = (await response.json());
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
fetchError = `Failed to fetch issues: ${err instanceof Error ? err.message : String(err)}`;
|
|
28
|
+
}
|
|
29
|
+
const result = formatIssues(issues, fetchError);
|
|
30
|
+
cache = { data: result, fetchedAt: now };
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function formatIssues(issues, fetchError) {
|
|
34
|
+
const lines = ["# SceneView — Open GitHub Issues\n"];
|
|
35
|
+
if (fetchError) {
|
|
36
|
+
lines.push(`> ⚠️ ${fetchError}. Showing cached data if available.\n`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
lines.push(`*Fetched live from GitHub — ${issues.length} open issue(s). Cached for 10 minutes.*\n`);
|
|
40
|
+
}
|
|
41
|
+
if (issues.length === 0) {
|
|
42
|
+
lines.push("No open issues. 🎉");
|
|
43
|
+
return lines.join("\n");
|
|
44
|
+
}
|
|
45
|
+
// Group by label: bugs first, then questions/other
|
|
46
|
+
const bugs = issues.filter((i) => i.labels.some((l) => l.name === "bug"));
|
|
47
|
+
const others = issues.filter((i) => !i.labels.some((l) => l.name === "bug"));
|
|
48
|
+
const renderGroup = (title, group) => {
|
|
49
|
+
if (group.length === 0)
|
|
50
|
+
return;
|
|
51
|
+
lines.push(`## ${title}\n`);
|
|
52
|
+
for (const issue of group) {
|
|
53
|
+
const labelStr = issue.labels.length > 0 ? ` [${issue.labels.map((l) => l.name).join(", ")}]` : "";
|
|
54
|
+
lines.push(`### #${issue.number} — ${issue.title}${labelStr}`);
|
|
55
|
+
lines.push(`*Opened by @${issue.user.login} · ${issue.updated_at.slice(0, 10)}*`);
|
|
56
|
+
lines.push(issue.html_url);
|
|
57
|
+
// Include first 300 chars of body as context
|
|
58
|
+
if (issue.body) {
|
|
59
|
+
const excerpt = issue.body
|
|
60
|
+
.replace(/\r\n/g, "\n")
|
|
61
|
+
.replace(/```[\s\S]*?```/g, "[code block]") // strip code blocks for brevity
|
|
62
|
+
.trim()
|
|
63
|
+
.slice(0, 300);
|
|
64
|
+
lines.push(`\n> ${excerpt.replace(/\n/g, "\n> ")}${issue.body.length > 300 ? "…" : ""}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push("");
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
renderGroup("🐛 Bug Reports", bugs);
|
|
70
|
+
renderGroup("📋 Other Issues", others);
|
|
71
|
+
return lines.join("\n");
|
|
72
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
// We test the private formatIssues logic by calling fetchKnownIssues with a
|
|
3
|
+
// mocked global fetch so we never hit the real GitHub API in tests.
|
|
4
|
+
const mockIssues = [
|
|
5
|
+
{
|
|
6
|
+
number: 123,
|
|
7
|
+
title: "SIGABRT on dispose",
|
|
8
|
+
html_url: "https://github.com/SceneView/sceneview-android/issues/123",
|
|
9
|
+
body: "Calling destroy() causes a native crash.",
|
|
10
|
+
labels: [{ name: "bug" }],
|
|
11
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
12
|
+
updated_at: "2026-01-15T00:00:00Z",
|
|
13
|
+
user: { login: "testuser" },
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
number: 124,
|
|
17
|
+
title: "How do I add shadows?",
|
|
18
|
+
html_url: "https://github.com/SceneView/sceneview-android/issues/124",
|
|
19
|
+
body: "I want to enable shadows for my AR scene.",
|
|
20
|
+
labels: [{ name: "question" }],
|
|
21
|
+
created_at: "2026-01-02T00:00:00Z",
|
|
22
|
+
updated_at: "2026-01-16T00:00:00Z",
|
|
23
|
+
user: { login: "anotheruser" },
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
// Reset module cache so the in-memory cache is cleared between tests
|
|
28
|
+
vi.resetModules();
|
|
29
|
+
});
|
|
30
|
+
describe("fetchKnownIssues", () => {
|
|
31
|
+
it("returns markdown with issue titles when GitHub API succeeds", async () => {
|
|
32
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: async () => mockIssues,
|
|
35
|
+
}));
|
|
36
|
+
const { fetchKnownIssues } = await import("./issues.js");
|
|
37
|
+
const result = await fetchKnownIssues();
|
|
38
|
+
expect(result).toContain("SIGABRT on dispose");
|
|
39
|
+
expect(result).toContain("#123");
|
|
40
|
+
expect(result).toContain("How do I add shadows?");
|
|
41
|
+
expect(result).toContain("#124");
|
|
42
|
+
vi.unstubAllGlobals();
|
|
43
|
+
});
|
|
44
|
+
it("groups bug-labelled issues under Bug Reports", async () => {
|
|
45
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
46
|
+
ok: true,
|
|
47
|
+
json: async () => mockIssues,
|
|
48
|
+
}));
|
|
49
|
+
const { fetchKnownIssues } = await import("./issues.js");
|
|
50
|
+
const result = await fetchKnownIssues();
|
|
51
|
+
expect(result).toContain("Bug Reports");
|
|
52
|
+
expect(result).toContain("SIGABRT on dispose");
|
|
53
|
+
vi.unstubAllGlobals();
|
|
54
|
+
});
|
|
55
|
+
it("groups non-bug issues under Other Issues", async () => {
|
|
56
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
57
|
+
ok: true,
|
|
58
|
+
json: async () => mockIssues,
|
|
59
|
+
}));
|
|
60
|
+
const { fetchKnownIssues } = await import("./issues.js");
|
|
61
|
+
const result = await fetchKnownIssues();
|
|
62
|
+
expect(result).toContain("Other Issues");
|
|
63
|
+
expect(result).toContain("How do I add shadows?");
|
|
64
|
+
vi.unstubAllGlobals();
|
|
65
|
+
});
|
|
66
|
+
it("includes github URLs", async () => {
|
|
67
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
68
|
+
ok: true,
|
|
69
|
+
json: async () => mockIssues,
|
|
70
|
+
}));
|
|
71
|
+
const { fetchKnownIssues } = await import("./issues.js");
|
|
72
|
+
const result = await fetchKnownIssues();
|
|
73
|
+
expect(result).toContain("https://github.com/SceneView/sceneview-android/issues/123");
|
|
74
|
+
vi.unstubAllGlobals();
|
|
75
|
+
});
|
|
76
|
+
it("includes body excerpt", async () => {
|
|
77
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
78
|
+
ok: true,
|
|
79
|
+
json: async () => mockIssues,
|
|
80
|
+
}));
|
|
81
|
+
const { fetchKnownIssues } = await import("./issues.js");
|
|
82
|
+
const result = await fetchKnownIssues();
|
|
83
|
+
expect(result).toContain("native crash");
|
|
84
|
+
vi.unstubAllGlobals();
|
|
85
|
+
});
|
|
86
|
+
it("shows warning message when GitHub API fails", async () => {
|
|
87
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
88
|
+
ok: false,
|
|
89
|
+
status: 403,
|
|
90
|
+
statusText: "Forbidden",
|
|
91
|
+
}));
|
|
92
|
+
const { fetchKnownIssues } = await import("./issues.js");
|
|
93
|
+
const result = await fetchKnownIssues();
|
|
94
|
+
expect(result).toContain("403");
|
|
95
|
+
vi.unstubAllGlobals();
|
|
96
|
+
});
|
|
97
|
+
it("shows warning message when fetch throws (network error)", async () => {
|
|
98
|
+
vi.stubGlobal("fetch", vi.fn().mockRejectedValue(new Error("Network error")));
|
|
99
|
+
const { fetchKnownIssues } = await import("./issues.js");
|
|
100
|
+
const result = await fetchKnownIssues();
|
|
101
|
+
expect(result).toContain("Network error");
|
|
102
|
+
vi.unstubAllGlobals();
|
|
103
|
+
});
|
|
104
|
+
it("shows celebration when there are no open issues", async () => {
|
|
105
|
+
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
|
|
106
|
+
ok: true,
|
|
107
|
+
json: async () => [],
|
|
108
|
+
}));
|
|
109
|
+
const { fetchKnownIssues } = await import("./issues.js");
|
|
110
|
+
const result = await fetchKnownIssues();
|
|
111
|
+
expect(result).toContain("No open issues");
|
|
112
|
+
vi.unstubAllGlobals();
|
|
113
|
+
});
|
|
114
|
+
});
|