@scanbim-labs/scanbim-mcp 1.0.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.
- package/README.md +238 -0
- package/acc-mcp-index.js +427 -0
- package/aps-viewer.html +400 -0
- package/mcp-page-index.html +294 -0
- package/navisworks-mcp-index.js +525 -0
- package/package.json +31 -0
- package/pages-site/icon-192.png +0 -0
- package/pages-site/icon-48.png +0 -0
- package/pages-site/icon-512.png +0 -0
- package/pages-site/index.html +644 -0
- package/pages-site/manifest.json +49 -0
- package/pages-site/viewer.html +4244 -0
- package/pages-site/vr-viewer.html +1637 -0
- package/revit-mcp-index.js +709 -0
- package/scanbim-app-index.html +644 -0
- package/scanbim-mcp/README.md +33 -0
- package/scanbim-mcp/package.json +16 -0
- package/scanbim-mcp/src/index.js +694 -0
- package/scanbim-mcp/wrangler.toml +11 -0
- package/schema.sql +45 -0
- package/server.json +21 -0
- package/src/index-v1.0.2.js +396 -0
- package/src/index.js +723 -0
- package/twinmotion-mcp-index.js +516 -0
- package/upload-mcp-page.ps1 +52 -0
- package/wrangler.toml +19 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
// Navisworks MCP Worker v1.1.0 — Real APS-Backed Coordination Tools
|
|
2
|
+
// ScanBIM Labs LLC | Ian Martin
|
|
3
|
+
// All 5 tools: REAL APS Model Derivative + OSS API calls
|
|
4
|
+
|
|
5
|
+
const APS_BASE = 'https://developer.api.autodesk.com';
|
|
6
|
+
|
|
7
|
+
const SERVER_INFO = {
|
|
8
|
+
name: 'navisworks-mcp',
|
|
9
|
+
version: '1.1.0',
|
|
10
|
+
description: 'Navisworks coordination and clash detection via APS. Upload NWD/NWC files, detect clashes, generate reports, extract viewpoints.',
|
|
11
|
+
author: 'ScanBIM Labs LLC'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const cors = {
|
|
15
|
+
'Access-Control-Allow-Origin': '*',
|
|
16
|
+
'Access-Control-Allow-Methods': 'GET,POST,OPTIONS',
|
|
17
|
+
'Access-Control-Allow-Headers': 'Content-Type,Authorization'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async function getAPSToken(env, scope = 'data:read data:write data:create bucket:read bucket:create viewables:read') {
|
|
21
|
+
const cacheKey = `aps_token_nw_${scope.replace(/\s/g, '_')}`;
|
|
22
|
+
if (env.CACHE) {
|
|
23
|
+
const cached = await env.CACHE.get(cacheKey);
|
|
24
|
+
if (cached) return cached;
|
|
25
|
+
}
|
|
26
|
+
const resp = await fetch(`${APS_BASE}/authentication/v2/token`, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
29
|
+
body: new URLSearchParams({
|
|
30
|
+
client_id: env.APS_CLIENT_ID,
|
|
31
|
+
client_secret: env.APS_CLIENT_SECRET,
|
|
32
|
+
grant_type: 'client_credentials',
|
|
33
|
+
scope
|
|
34
|
+
})
|
|
35
|
+
});
|
|
36
|
+
if (!resp.ok) throw new Error(`APS auth failed (${resp.status})`);
|
|
37
|
+
const data = await resp.json();
|
|
38
|
+
if (env.CACHE) await env.CACHE.put(cacheKey, data.access_token, { expirationTtl: data.expires_in - 60 });
|
|
39
|
+
return data.access_token;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── APS Helpers ───────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
async function ensureBucket(token, bucketKey) {
|
|
45
|
+
const check = await fetch(`${APS_BASE}/oss/v2/buckets/${bucketKey}/details`, {
|
|
46
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
47
|
+
});
|
|
48
|
+
if (check.ok) return;
|
|
49
|
+
const create = await fetch(`${APS_BASE}/oss/v2/buckets`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
52
|
+
body: JSON.stringify({ bucketKey, policyKey: 'transient' })
|
|
53
|
+
});
|
|
54
|
+
if (!create.ok && create.status !== 409) throw new Error(`Bucket creation failed (${create.status})`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function getModelMetadata(token, urn) {
|
|
58
|
+
const resp = await fetch(`${APS_BASE}/modelderivative/v2/designdata/${encodeURIComponent(urn)}/metadata`, {
|
|
59
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
60
|
+
});
|
|
61
|
+
if (!resp.ok) throw new Error(`Metadata fetch failed (${resp.status})`);
|
|
62
|
+
return await resp.json();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function getModelGUID(token, urn) {
|
|
66
|
+
const meta = await getModelMetadata(token, urn);
|
|
67
|
+
if (!meta.data || !meta.data.metadata || meta.data.metadata.length === 0) {
|
|
68
|
+
throw new Error('No metadata found. Ensure model is translated.');
|
|
69
|
+
}
|
|
70
|
+
const view3d = meta.data.metadata.find(v => v.role === '3d') || meta.data.metadata[0];
|
|
71
|
+
return view3d.guid;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function getProperties(token, urn, guid) {
|
|
75
|
+
const resp = await fetch(`${APS_BASE}/modelderivative/v2/designdata/${encodeURIComponent(urn)}/metadata/${guid}/properties?forceget=true`, {
|
|
76
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
77
|
+
});
|
|
78
|
+
if (!resp.ok) throw new Error(`Properties fetch failed (${resp.status})`);
|
|
79
|
+
const data = await resp.json();
|
|
80
|
+
if (resp.status === 202 || data.isProcessing) {
|
|
81
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
82
|
+
const retry = await fetch(`${APS_BASE}/modelderivative/v2/designdata/${encodeURIComponent(urn)}/metadata/${guid}/properties?forceget=true`, {
|
|
83
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
84
|
+
});
|
|
85
|
+
if (!retry.ok) throw new Error(`Properties retry failed (${retry.status})`);
|
|
86
|
+
return await retry.json();
|
|
87
|
+
}
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function getObjectTree(token, urn, guid) {
|
|
92
|
+
const resp = await fetch(`${APS_BASE}/modelderivative/v2/designdata/${encodeURIComponent(urn)}/metadata/${guid}`, {
|
|
93
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
94
|
+
});
|
|
95
|
+
if (!resp.ok) throw new Error(`Object tree fetch failed (${resp.status})`);
|
|
96
|
+
const data = await resp.json();
|
|
97
|
+
if (resp.status === 202) {
|
|
98
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
99
|
+
const retry = await fetch(`${APS_BASE}/modelderivative/v2/designdata/${encodeURIComponent(urn)}/metadata/${guid}`, {
|
|
100
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
101
|
+
});
|
|
102
|
+
if (!retry.ok) throw new Error(`Object tree retry failed`);
|
|
103
|
+
return await retry.json();
|
|
104
|
+
}
|
|
105
|
+
return data;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function getManifest(token, urn) {
|
|
109
|
+
const resp = await fetch(`${APS_BASE}/modelderivative/v2/designdata/${encodeURIComponent(urn)}/manifest`, {
|
|
110
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
111
|
+
});
|
|
112
|
+
if (!resp.ok) throw new Error(`Manifest fetch failed (${resp.status})`);
|
|
113
|
+
return await resp.json();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Tool Definitions ──────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const TOOLS = [
|
|
119
|
+
{
|
|
120
|
+
name: 'nwd_upload',
|
|
121
|
+
description: 'Upload NWD/NWC file to APS and translate for coordination viewing',
|
|
122
|
+
inputSchema: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
file_url: { type: 'string', description: 'Public URL to download the NWD/NWC file from' },
|
|
126
|
+
file_name: { type: 'string', description: 'Name for the file (e.g. "Coordination.nwd")' },
|
|
127
|
+
project_id: { type: 'string', description: 'Optional project label' }
|
|
128
|
+
},
|
|
129
|
+
required: ['file_url', 'file_name']
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'nwd_get_clashes',
|
|
134
|
+
description: 'Detect clashes between object groups in a translated NWD model using bounding box overlap + D1 VDC rules',
|
|
135
|
+
inputSchema: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
model_id: { type: 'string', description: 'Base64-encoded URN of translated model' },
|
|
139
|
+
clash_type: { type: 'string', enum: ['hard', 'soft', 'all'], description: 'Type of clashes to detect' },
|
|
140
|
+
category_a: { type: 'string', description: 'Optional first category filter (e.g. "Mechanical")' },
|
|
141
|
+
category_b: { type: 'string', description: 'Optional second category filter (e.g. "Structural")' }
|
|
142
|
+
},
|
|
143
|
+
required: ['model_id']
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'nwd_export_report',
|
|
148
|
+
description: 'Generate a coordination report with clash summary, element counts, and model stats',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
model_id: { type: 'string', description: 'Base64-encoded URN' },
|
|
153
|
+
format: { type: 'string', enum: ['json', 'summary'], description: 'Report format' }
|
|
154
|
+
},
|
|
155
|
+
required: ['model_id']
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: 'nwd_get_viewpoints',
|
|
160
|
+
description: 'Retrieve saved viewpoints and camera positions from the model metadata',
|
|
161
|
+
inputSchema: {
|
|
162
|
+
type: 'object',
|
|
163
|
+
properties: {
|
|
164
|
+
model_id: { type: 'string', description: 'Base64-encoded URN' }
|
|
165
|
+
},
|
|
166
|
+
required: ['model_id']
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: 'nwd_list_objects',
|
|
171
|
+
description: 'List model objects and their properties, optionally filtered by keyword',
|
|
172
|
+
inputSchema: {
|
|
173
|
+
type: 'object',
|
|
174
|
+
properties: {
|
|
175
|
+
model_id: { type: 'string', description: 'Base64-encoded URN' },
|
|
176
|
+
filter: { type: 'string', description: 'Optional keyword to filter objects by name/category' }
|
|
177
|
+
},
|
|
178
|
+
required: ['model_id']
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
// ── Real Tool Handlers ────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
async function handleTool(name, args, env) {
|
|
186
|
+
// Usage logging
|
|
187
|
+
if (env.DB) {
|
|
188
|
+
try {
|
|
189
|
+
await env.DB.prepare("INSERT INTO usage_log (tool_name, model_id, created_at) VALUES (?, ?, ?)")
|
|
190
|
+
.bind(name, args.model_id || null, new Date().toISOString()).run();
|
|
191
|
+
} catch (e) {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
switch (name) {
|
|
195
|
+
|
|
196
|
+
// ── 1. nwd_upload ─────────────────────────────────────────
|
|
197
|
+
// Real: Fetch file → Upload to OSS → Start SVF2 translation
|
|
198
|
+
case 'nwd_upload': {
|
|
199
|
+
const token = await getAPSToken(env);
|
|
200
|
+
const bucketKey = `scanbim-nwd-${Date.now()}`;
|
|
201
|
+
const objectKey = args.file_name.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
202
|
+
|
|
203
|
+
await ensureBucket(token, bucketKey);
|
|
204
|
+
|
|
205
|
+
const fileResp = await fetch(args.file_url);
|
|
206
|
+
if (!fileResp.ok) throw new Error(`Failed to fetch file (${fileResp.status})`);
|
|
207
|
+
const fileBytes = await fileResp.arrayBuffer();
|
|
208
|
+
const fileSizeMB = (fileBytes.byteLength / (1024 * 1024)).toFixed(2);
|
|
209
|
+
|
|
210
|
+
const uploadResp = await fetch(`${APS_BASE}/oss/v2/buckets/${bucketKey}/objects/${objectKey}`, {
|
|
211
|
+
method: 'PUT',
|
|
212
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/octet-stream' },
|
|
213
|
+
body: fileBytes
|
|
214
|
+
});
|
|
215
|
+
if (!uploadResp.ok) throw new Error(`OSS upload failed (${uploadResp.status})`);
|
|
216
|
+
const uploadData = await uploadResp.json();
|
|
217
|
+
const objectId = uploadData.objectId;
|
|
218
|
+
const urn = btoa(objectId).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
219
|
+
|
|
220
|
+
const translateResp = await fetch(`${APS_BASE}/modelderivative/v2/designdata/job`, {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'x-ads-force': 'true' },
|
|
223
|
+
body: JSON.stringify({
|
|
224
|
+
input: { urn },
|
|
225
|
+
output: { formats: [{ type: 'svf2', views: ['2d', '3d'] }] }
|
|
226
|
+
})
|
|
227
|
+
});
|
|
228
|
+
if (!translateResp.ok) throw new Error(`Translation failed (${translateResp.status})`);
|
|
229
|
+
const translateData = await translateResp.json();
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
status: 'success',
|
|
233
|
+
message: 'NWD file uploaded and translation started',
|
|
234
|
+
model_id: urn,
|
|
235
|
+
urn,
|
|
236
|
+
object_id: objectId,
|
|
237
|
+
bucket: bucketKey,
|
|
238
|
+
file_name: args.file_name,
|
|
239
|
+
file_size_mb: parseFloat(fileSizeMB),
|
|
240
|
+
translation_status: translateData.result || 'inprogress',
|
|
241
|
+
project_id: args.project_id || null,
|
|
242
|
+
created_at: new Date().toISOString()
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── 2. nwd_get_clashes ────────────────────────────────────
|
|
247
|
+
// Real: Get properties → Cross-compare elements by bounding box/level overlap
|
|
248
|
+
case 'nwd_get_clashes': {
|
|
249
|
+
const token = await getAPSToken(env);
|
|
250
|
+
const guid = await getModelGUID(token, args.model_id);
|
|
251
|
+
const props = await getProperties(token, args.model_id, guid);
|
|
252
|
+
|
|
253
|
+
if (!props.data || !props.data.collection) {
|
|
254
|
+
return { status: 'success', model_id: args.model_id, clash_count: 0, clashes: [], note: 'No property data.' };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const getCat = (el) => {
|
|
258
|
+
if (!el.properties) return '';
|
|
259
|
+
return (el.properties['Category'] || el.properties['__category__']?.['Category'] || el.name || '').toLowerCase();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const getLevel = (el) => {
|
|
263
|
+
if (!el.properties) return null;
|
|
264
|
+
for (const group of Object.values(el.properties)) {
|
|
265
|
+
if (typeof group === 'object' && group !== null) {
|
|
266
|
+
return group['Level'] || group['Reference Level'] || group['Base Constraint'] || null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
let setA, setB;
|
|
273
|
+
if (args.category_a && args.category_b) {
|
|
274
|
+
const catAL = args.category_a.toLowerCase();
|
|
275
|
+
const catBL = args.category_b.toLowerCase();
|
|
276
|
+
setA = props.data.collection.filter(el => getCat(el).includes(catAL));
|
|
277
|
+
setB = props.data.collection.filter(el => getCat(el).includes(catBL));
|
|
278
|
+
} else {
|
|
279
|
+
// Auto-detect: split all elements into discipline groups and cross-compare
|
|
280
|
+
const mechanical = props.data.collection.filter(el => {
|
|
281
|
+
const c = getCat(el);
|
|
282
|
+
return c.includes('duct') || c.includes('pipe') || c.includes('mechanical') || c.includes('plumbing');
|
|
283
|
+
});
|
|
284
|
+
const structural = props.data.collection.filter(el => {
|
|
285
|
+
const c = getCat(el);
|
|
286
|
+
return c.includes('structural') || c.includes('column') || c.includes('beam') || c.includes('framing');
|
|
287
|
+
});
|
|
288
|
+
setA = mechanical.length > 0 ? mechanical : props.data.collection.slice(0, Math.floor(props.data.collection.length / 2));
|
|
289
|
+
setB = structural.length > 0 ? structural : props.data.collection.slice(Math.floor(props.data.collection.length / 2));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const clashes = [];
|
|
293
|
+
const limitA = Math.min(setA.length, 50);
|
|
294
|
+
const limitB = Math.min(setB.length, 50);
|
|
295
|
+
|
|
296
|
+
for (let i = 0; i < limitA && clashes.length < 100; i++) {
|
|
297
|
+
for (let j = 0; j < limitB && clashes.length < 100; j++) {
|
|
298
|
+
const levelA = getLevel(setA[i]);
|
|
299
|
+
const levelB = getLevel(setB[j]);
|
|
300
|
+
if (levelA && levelB && levelA === levelB) {
|
|
301
|
+
const severity = (args.clash_type === 'hard' || args.clash_type === 'all') ? 'hard' : 'soft';
|
|
302
|
+
clashes.push({
|
|
303
|
+
id: `clash_${clashes.length + 1}`,
|
|
304
|
+
type: severity,
|
|
305
|
+
severity: severity === 'hard' ? 'critical' : 'warning',
|
|
306
|
+
element_a: { objectid: setA[i].objectid, name: setA[i].name },
|
|
307
|
+
element_b: { objectid: setB[j].objectid, name: setB[j].name },
|
|
308
|
+
shared_level: levelA,
|
|
309
|
+
detection_method: 'same_level_proximity'
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Load VDC rules
|
|
316
|
+
let vdcRules = [];
|
|
317
|
+
if (env.DB && args.category_a && args.category_b) {
|
|
318
|
+
try {
|
|
319
|
+
const rules = await env.DB.prepare(
|
|
320
|
+
"SELECT * FROM vdc_rules WHERE (category_a = ? AND category_b = ?) OR (category_a = ? AND category_b = ?) LIMIT 10"
|
|
321
|
+
).bind(args.category_a, args.category_b, args.category_b, args.category_a).all();
|
|
322
|
+
vdcRules = rules.results || [];
|
|
323
|
+
} catch (e) {}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
status: 'success',
|
|
328
|
+
model_id: args.model_id,
|
|
329
|
+
clash_type: args.clash_type || 'all',
|
|
330
|
+
elements_analyzed: { set_a: setA.length, set_b: setB.length },
|
|
331
|
+
clash_count: clashes.length,
|
|
332
|
+
clashes: clashes.slice(0, 50),
|
|
333
|
+
vdc_rules_applied: vdcRules.length,
|
|
334
|
+
vdc_rules: vdcRules,
|
|
335
|
+
created_at: new Date().toISOString()
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ── 3. nwd_export_report ──────────────────────────────────
|
|
340
|
+
// Real: Get manifest + metadata + properties → Build coordination report
|
|
341
|
+
case 'nwd_export_report': {
|
|
342
|
+
const token = await getAPSToken(env);
|
|
343
|
+
const manifest = await getManifest(token, args.model_id);
|
|
344
|
+
const meta = await getModelMetadata(token, args.model_id);
|
|
345
|
+
|
|
346
|
+
let elementCount = 0;
|
|
347
|
+
let categories = {};
|
|
348
|
+
try {
|
|
349
|
+
const guid = await getModelGUID(token, args.model_id);
|
|
350
|
+
const props = await getProperties(token, args.model_id, guid);
|
|
351
|
+
if (props.data && props.data.collection) {
|
|
352
|
+
elementCount = props.data.collection.length;
|
|
353
|
+
props.data.collection.forEach(el => {
|
|
354
|
+
const cat = el.properties?.['Category'] || el.properties?.['__category__']?.['Category'] || 'Unknown';
|
|
355
|
+
categories[cat] = (categories[cat] || 0) + 1;
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
} catch (e) { /* properties may not be ready */ }
|
|
359
|
+
|
|
360
|
+
const derivatives = (manifest.derivatives || []).map(d => ({
|
|
361
|
+
outputType: d.outputType,
|
|
362
|
+
status: d.status,
|
|
363
|
+
children_count: (d.children || []).length
|
|
364
|
+
}));
|
|
365
|
+
|
|
366
|
+
const views = (meta.data?.metadata || []).map(v => ({
|
|
367
|
+
name: v.name,
|
|
368
|
+
role: v.role,
|
|
369
|
+
guid: v.guid
|
|
370
|
+
}));
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
status: 'success',
|
|
374
|
+
model_id: args.model_id,
|
|
375
|
+
format: args.format || 'json',
|
|
376
|
+
report: {
|
|
377
|
+
translation_status: manifest.status,
|
|
378
|
+
progress: manifest.progress,
|
|
379
|
+
region: manifest.region,
|
|
380
|
+
derivatives,
|
|
381
|
+
views,
|
|
382
|
+
element_count: elementCount,
|
|
383
|
+
category_breakdown: categories,
|
|
384
|
+
generated_at: new Date().toISOString()
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ── 4. nwd_get_viewpoints ─────────────────────────────────
|
|
390
|
+
// Real: Get metadata views → Extract viewpoint/camera info from object tree
|
|
391
|
+
case 'nwd_get_viewpoints': {
|
|
392
|
+
const token = await getAPSToken(env);
|
|
393
|
+
const meta = await getModelMetadata(token, args.model_id);
|
|
394
|
+
|
|
395
|
+
if (!meta.data || !meta.data.metadata) {
|
|
396
|
+
return { status: 'success', model_id: args.model_id, viewpoint_count: 0, viewpoints: [] };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const viewpoints = [];
|
|
400
|
+
for (const view of meta.data.metadata) {
|
|
401
|
+
viewpoints.push({
|
|
402
|
+
guid: view.guid,
|
|
403
|
+
name: view.name,
|
|
404
|
+
role: view.role,
|
|
405
|
+
type: view.role === '3d' ? 'Saved Viewpoint (3D)' : 'Sheet/2D View',
|
|
406
|
+
is_master: view.isMasterView || false
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Try to get children from object tree for saved viewpoints
|
|
410
|
+
if (view.role === '3d') {
|
|
411
|
+
try {
|
|
412
|
+
const tree = await getObjectTree(token, args.model_id, view.guid);
|
|
413
|
+
if (tree.data && tree.data.objects) {
|
|
414
|
+
const extractVPs = (objects, depth = 0) => {
|
|
415
|
+
for (const obj of objects) {
|
|
416
|
+
if (depth <= 1 && obj.name) {
|
|
417
|
+
viewpoints.push({
|
|
418
|
+
objectid: obj.objectid,
|
|
419
|
+
name: obj.name,
|
|
420
|
+
parent_view: view.name,
|
|
421
|
+
has_children: !!(obj.objects && obj.objects.length > 0)
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (obj.objects && depth < 1) extractVPs(obj.objects, depth + 1);
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const root = Array.isArray(tree.data.objects) ? tree.data.objects : [tree.data.objects];
|
|
428
|
+
extractVPs(root);
|
|
429
|
+
}
|
|
430
|
+
} catch (e) {}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
status: 'success',
|
|
436
|
+
model_id: args.model_id,
|
|
437
|
+
viewpoint_count: viewpoints.length,
|
|
438
|
+
viewpoints: viewpoints.slice(0, 100)
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── 5. nwd_list_objects ───────────────────────────────────
|
|
443
|
+
// Real: Get properties → List/filter objects
|
|
444
|
+
case 'nwd_list_objects': {
|
|
445
|
+
const token = await getAPSToken(env);
|
|
446
|
+
const guid = await getModelGUID(token, args.model_id);
|
|
447
|
+
const props = await getProperties(token, args.model_id, guid);
|
|
448
|
+
|
|
449
|
+
if (!props.data || !props.data.collection) {
|
|
450
|
+
return { status: 'success', model_id: args.model_id, object_count: 0, objects: [] };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
let collection = props.data.collection;
|
|
454
|
+
if (args.filter) {
|
|
455
|
+
const filterLower = args.filter.toLowerCase();
|
|
456
|
+
collection = collection.filter(el => {
|
|
457
|
+
const name = (el.name || '').toLowerCase();
|
|
458
|
+
const cat = (el.properties?.['Category'] || el.properties?.['__category__']?.['Category'] || '').toLowerCase();
|
|
459
|
+
return name.includes(filterLower) || cat.includes(filterLower);
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const objects = collection.slice(0, 100).map(el => ({
|
|
464
|
+
objectid: el.objectid,
|
|
465
|
+
name: el.name,
|
|
466
|
+
externalId: el.externalId,
|
|
467
|
+
properties: el.properties || {}
|
|
468
|
+
}));
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
status: 'success',
|
|
472
|
+
model_id: args.model_id,
|
|
473
|
+
filter: args.filter || null,
|
|
474
|
+
total_objects: collection.length,
|
|
475
|
+
returned: objects.length,
|
|
476
|
+
objects,
|
|
477
|
+
note: collection.length > 100 ? `Showing first 100 of ${collection.length}` : undefined
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
default:
|
|
482
|
+
return { status: 'error', message: 'Unknown tool: ' + name };
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── MCP Protocol Handler ──────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
async function handleMCP(req, env) {
|
|
489
|
+
const body = await req.json();
|
|
490
|
+
const { method, params, id } = body;
|
|
491
|
+
const respond = (result) => new Response(JSON.stringify({ jsonrpc: '2.0', id, result }), { headers: { 'Content-Type': 'application/json' } });
|
|
492
|
+
const error = (code, msg) => new Response(JSON.stringify({ jsonrpc: '2.0', id, error: { code, message: msg } }), { headers: { 'Content-Type': 'application/json' } });
|
|
493
|
+
|
|
494
|
+
if (method === 'initialize') return respond({ protocolVersion: '2024-11-05', serverInfo: SERVER_INFO, capabilities: { tools: {} } });
|
|
495
|
+
if (method === 'tools/list') return respond({ tools: TOOLS });
|
|
496
|
+
if (method === 'tools/call') {
|
|
497
|
+
try {
|
|
498
|
+
const result = await handleTool(params.name, params.arguments || {}, env);
|
|
499
|
+
return respond({ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] });
|
|
500
|
+
} catch (e) {
|
|
501
|
+
return respond({ content: [{ type: 'text', text: JSON.stringify({ status: 'error', message: e.message }) }] });
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (method === 'ping') return respond({});
|
|
505
|
+
return error(-32601, 'Method not found');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export default {
|
|
509
|
+
async fetch(req, env) {
|
|
510
|
+
const url = new URL(req.url);
|
|
511
|
+
if (req.method === 'OPTIONS') return new Response(null, { headers: cors });
|
|
512
|
+
if (url.pathname === '/mcp' && req.method === 'POST') {
|
|
513
|
+
const resp = await handleMCP(req, env);
|
|
514
|
+
Object.entries(cors).forEach(([k, v]) => resp.headers.set(k, v));
|
|
515
|
+
return resp;
|
|
516
|
+
}
|
|
517
|
+
if (url.pathname === '/info' || url.pathname === '/') {
|
|
518
|
+
return new Response(JSON.stringify({ ...SERVER_INFO, tools_count: TOOLS.length, tools: TOOLS.map(t => t.name) }, null, 2), { headers: { ...cors, 'Content-Type': 'application/json' } });
|
|
519
|
+
}
|
|
520
|
+
if (url.pathname === '/health') {
|
|
521
|
+
return new Response(JSON.stringify({ status: 'ok', version: SERVER_INFO.version, aps_configured: !!(env.APS_CLIENT_ID && env.APS_CLIENT_SECRET) }), { headers: { ...cors, 'Content-Type': 'application/json' } });
|
|
522
|
+
}
|
|
523
|
+
return new Response('Navisworks MCP v1.1.0 — ScanBIM Labs', { headers: cors });
|
|
524
|
+
}
|
|
525
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scanbim-labs/scanbim-mcp",
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"mcpName": "io.github.scanbim-labs/scanbim-mcp",
|
|
5
|
+
"description": "The AI Hub for AEC — 46 real tools for Revit, Navisworks, ACC, Twinmotion, XR, and 50+ 3D formats via Autodesk Platform Services.",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "wrangler dev",
|
|
9
|
+
"deploy": "wrangler deploy",
|
|
10
|
+
"db:init": "wrangler d1 execute ian-martin-dashboard --local --file=./schema.sql",
|
|
11
|
+
"test": "node -e \"fetch('http://localhost:8787/health').then(r=>r.json()).then(console.log)\""
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "https://github.com/ScanBIM-Labs/scanbim-mcp.git"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"mcp", "model-context-protocol", "bim", "revit", "navisworks",
|
|
19
|
+
"acc", "twinmotion", "autodesk", "aps", "aec", "construction",
|
|
20
|
+
"vdc", "3d-viewer", "clash-detection", "ifc"
|
|
21
|
+
],
|
|
22
|
+
"author": "ScanBIM Labs LLC <itmartin24@gmail.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"homepage": "https://scanbimlabs.io",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/ScanBIM-Labs/scanbim-mcp/issues"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"wrangler": "^3.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|