@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 ADDED
@@ -0,0 +1,238 @@
1
+ # ScanBIM MCP — The AI Hub for AEC
2
+
3
+ [![Live](https://img.shields.io/badge/status-live-brightgreen)](https://scanbim-mcp.itmartin24.workers.dev/)
4
+ [![MCP](https://img.shields.io/badge/protocol-MCP%202025--03--26-blue)](https://modelcontextprotocol.io)
5
+ [![Tools](https://img.shields.io/badge/tools-46%20real-orange)](https://scanbim-mcp.itmartin24.workers.dev/info)
6
+ [![APS](https://img.shields.io/badge/APS-connected-green)](https://aps.autodesk.com)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ **Give Claude, ChatGPT, and any AI agent the ability to upload, convert, view, analyze, and share BIM models across 50+ formats.**
10
+
11
+ Upload a .rvt file. Get back a shareable 3D viewer link with QR code. Run clash detection with 20 years of VDC intelligence. Create RFIs. Launch VR walkthroughs. No software install needed.
12
+
13
+ ---
14
+
15
+ ## Ecosystem (5 Workers, 46 Tools)
16
+
17
+ | Worker | Version | Tools | Endpoint | Description |
18
+ |--------|---------|-------|----------|-------------|
19
+ | **scanbim-mcp** | v1.0.5 | 19 | [/mcp](https://scanbim-mcp.itmartin24.workers.dev/mcp) | Core hub — models, clashes, ACC, XR, viewer, rendering |
20
+ | **revit-mcp** | v1.1.0 | 8 | [/mcp](https://revit-mcp.itmartin24.workers.dev/mcp) | Revit — elements, parameters, schedules, sheets, IFC export |
21
+ | **acc-mcp** | v1.0.1 | 9 | [/mcp](https://acc-mcp.itmartin24.workers.dev/mcp) | ACC/BIM 360 — issues, RFIs, documents, project summaries |
22
+ | **navisworks-mcp** | v1.1.0 | 5 | [/mcp](https://navisworks-mcp.itmartin24.workers.dev/mcp) | Navisworks — clash detection, coordination, viewpoints |
23
+ | **twinmotion-mcp** | v1.1.0 | 5 | [/mcp](https://twinmotion-mcp.itmartin24.workers.dev/mcp) | Visualization — renders, environments, video, scenes |
24
+
25
+ **All 46 tools make real Autodesk Platform Services API calls.** Zero stubs. Verified April 12, 2026.
26
+
27
+ ---
28
+
29
+ ## scanbim-mcp Tools (19)
30
+
31
+ | Tool | Description |
32
+ |------|-------------|
33
+ | `upload_model` | Upload 3D models (Revit, IFC, point clouds, 50+ formats) via APS OSS + SVF2 translation |
34
+ | `detect_clashes` | VDC-grade clash detection with D1 rules database (SMACNA, NEC, ACI 318) |
35
+ | `get_viewer_link` | Generate APS Viewer URL + QR code for any translated model |
36
+ | `list_models` | List all uploaded models in APS buckets |
37
+ | `get_model_metadata` | Get APS translation status, manifest, and metadata |
38
+ | `get_supported_formats` | List supported file formats by tier (free/pro/enterprise) |
39
+ | `acc_list_projects` | List ACC/BIM 360 hubs and projects |
40
+ | `acc_create_issue` | Create ACC issues with priority, assignment, due dates |
41
+ | `acc_list_issues` | List/filter ACC issues by status and priority |
42
+ | `acc_create_rfi` | Create ACC RFIs |
43
+ | `acc_list_rfis` | List/filter ACC RFIs |
44
+ | `acc_search_documents` | Search ACC project documents by keyword |
45
+ | `acc_project_summary` | Get project overview with issue/RFI counts |
46
+ | `xr_launch_vr_session` | Launch VR viewing session (Meta Quest 2/3/3S) |
47
+ | `xr_launch_ar_session` | Launch AR overlay session |
48
+ | `xr_list_sessions` | List active XR sessions |
49
+ | `twinmotion_render` | Generate photorealistic renders via APS |
50
+ | `twinmotion_walkthrough` | Create animated walkthrough sequences |
51
+ | `lumion_render` | Architectural visualization rendering |
52
+
53
+ ## revit-mcp Tools (8)
54
+
55
+ | Tool | Description |
56
+ |------|-------------|
57
+ | `revit_upload` | Upload .rvt files to APS with SVF2 translation |
58
+ | `revit_get_elements` | Extract elements by category (walls, doors, windows, etc.) |
59
+ | `revit_get_parameters` | Get element parameters with parameter group extraction |
60
+ | `revit_run_schedule` | Extract tabular schedule data from model properties |
61
+ | `revit_clash_detect` | Bounding box overlap + level proximity + D1 VDC rules |
62
+ | `revit_export_ifc` | Model Derivative IFC translation job |
63
+ | `revit_get_sheets` | List 2D views + sheet enumeration |
64
+ | `revit_get_views` | List all metadata views with detail levels |
65
+
66
+ ## acc-mcp Tools (9)
67
+
68
+ | Tool | Description |
69
+ |------|-------------|
70
+ | `acc_list_projects` | List all ACC/BIM 360 hubs and projects |
71
+ | `acc_create_issue` | Create quality/safety issues |
72
+ | `acc_update_issue` | Update issue status, priority, assignment |
73
+ | `acc_list_issues` | List/filter issues by status and priority |
74
+ | `acc_create_rfi` | Create RFIs with assignment and priority |
75
+ | `acc_list_rfis` | List/filter RFIs |
76
+ | `acc_search_documents` | Full-text document search across projects |
77
+ | `acc_upload_file` | Upload files via APS Data Management (4-step flow) |
78
+ | `acc_project_summary` | Project dashboard with hub/project/issue/RFI counts |
79
+
80
+ ## navisworks-mcp Tools (5)
81
+
82
+ | Tool | Description |
83
+ |------|-------------|
84
+ | `nwd_upload` | Upload .nwd/.nwc files with SVF2 translation |
85
+ | `nwd_get_clashes` | Cross-category clash analysis with level proximity + D1 VDC rules |
86
+ | `nwd_export_report` | Generate coordination report with category breakdown |
87
+ | `nwd_get_viewpoints` | Extract saved viewpoints and camera positions |
88
+ | `nwd_list_objects` | Property-based object listing with keyword filter |
89
+
90
+ ## twinmotion-mcp Tools (5)
91
+
92
+ | Tool | Description |
93
+ |------|-------------|
94
+ | `tm_import_rvt` | Import .rvt via APS with SVF2 + thumbnail translation |
95
+ | `tm_set_environment` | Configure environment settings (time, weather, season) |
96
+ | `tm_render_image` | APS thumbnail rendering with resolution control |
97
+ | `tm_export_video` | OBJ derivative for offline rendering pipeline |
98
+ | `tm_list_scenes` | Enumerate scenes from metadata views + object tree |
99
+
100
+ ---
101
+
102
+ ## Endpoints (scanbim-mcp)
103
+
104
+ | Path | Method | Description |
105
+ |------|--------|-------------|
106
+ | `/mcp` | POST | MCP JSON-RPC 2.0 endpoint (initialize, tools/list, tools/call, ping) |
107
+ | `/info` | GET | Server info, version, tool count, APS connection status |
108
+ | `/health` | GET | Health check with APS configuration status |
109
+ | `/token` | GET | APS access token (viewables:read scope) for Viewer JS integration |
110
+ | `/viewer?urn=XXX` | GET | Built-in APS Viewer JS v7 — load any translated model in-browser |
111
+
112
+ ---
113
+
114
+ ## Quick Start
115
+
116
+ ### Use the hosted MCP (Recommended)
117
+
118
+ Add to your Claude Desktop config:
119
+
120
+ ```json
121
+ {
122
+ "mcpServers": {
123
+ "scanbim": {
124
+ "url": "https://scanbim-mcp.itmartin24.workers.dev/mcp"
125
+ },
126
+ "revit": {
127
+ "url": "https://revit-mcp.itmartin24.workers.dev/mcp"
128
+ },
129
+ "acc": {
130
+ "url": "https://acc-mcp.itmartin24.workers.dev/mcp"
131
+ },
132
+ "navisworks": {
133
+ "url": "https://navisworks-mcp.itmartin24.workers.dev/mcp"
134
+ },
135
+ "twinmotion": {
136
+ "url": "https://twinmotion-mcp.itmartin24.workers.dev/mcp"
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ### curl
143
+
144
+ ```bash
145
+ # Health check
146
+ curl https://scanbim-mcp.itmartin24.workers.dev/health
147
+
148
+ # List tools
149
+ curl -X POST https://scanbim-mcp.itmartin24.workers.dev/mcp \
150
+ -H "Content-Type: application/json" \
151
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
152
+
153
+ # View a model
154
+ open "https://scanbim-mcp.itmartin24.workers.dev/viewer?urn=YOUR_BASE64_URN"
155
+ ```
156
+
157
+ ### Deploy your own
158
+
159
+ ```bash
160
+ git clone https://github.com/ScanBIM-Labs/scanbim-mcp.git
161
+ cd scanbim-mcp
162
+ npm install
163
+ npx wrangler secret put APS_CLIENT_ID
164
+ npx wrangler secret put APS_CLIENT_SECRET
165
+ npx wrangler deploy
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Architecture
171
+
172
+ ```
173
+ Claude / ChatGPT / Any AI Agent
174
+ | MCP Protocol (JSON-RPC 2.0)
175
+ v
176
+ Cloudflare Workers (5 workers, edge compute, <50ms global)
177
+ |-- scanbim-mcp (19 tools + /viewer + /token)
178
+ |-- revit-mcp (8 tools)
179
+ |-- acc-mcp (9 tools)
180
+ |-- navisworks-mcp (5 tools)
181
+ |-- twinmotion-mcp (5 tools)
182
+ |
183
+ |-- D1 Database (VDC rules, clash severity, coordination standards)
184
+ |-- KV Namespace (APS token caching with TTL)
185
+ |
186
+ v
187
+ Autodesk Platform Services (APS)
188
+ |-- Authentication v2 (2-legged client credentials)
189
+ |-- Model Derivative v2 (SVF2 translation, metadata, properties)
190
+ |-- Object Storage Service (file upload, bucket management)
191
+ |-- ACC Issues/RFIs API
192
+ |-- APS Viewer JS v7 (browser-based 3D rendering)
193
+ |
194
+ v
195
+ scanbim.app (Cloudflare Pages) + APS Viewer (/viewer route)
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Supported Formats (50+)
201
+
202
+ **Free:** IFC, glTF/GLB, OBJ, STL, PLY, E57, LAS/LAZ, DXF, DAE, 3DS, 3MF
203
+
204
+ **Pro ($49/mo):** + FBX, DWG, STEP/STP, IGES, SketchUp (.skp), DWF, SolidWorks (.sldprt/.sldasm), Inventor (.ipt/.iam), OSGB
205
+
206
+ **Enterprise ($149/mo):** + **Revit (.rvt/.rfa)**, **Navisworks (.nwd/.nwc)**, ReCap (.rcp/.rcs), PCD, PTS, FLS, PTX, PTG, ZFS, 3MX + 500M point clouds + ACC integration
207
+
208
+ ---
209
+
210
+ ## VDC Intelligence Engine
211
+
212
+ Clash detection powered by **20 years of field experience** encoded into D1-backed rules:
213
+
214
+ - **9 severity rules** — SMACNA, NEC, ACI 318, AISC, ASCE 7 standards
215
+ - **5 coordination standards** — MEP clearance, structural proximity
216
+ - **Fix suggestions** — Real construction advice, not generic "move element"
217
+ - **Rework estimation** — Hours-to-fix based on actual project data
218
+
219
+ ---
220
+
221
+ ## Links
222
+
223
+ - **APS Viewer:** https://scanbim-mcp.itmartin24.workers.dev/viewer
224
+ - **Health Check:** https://scanbim-mcp.itmartin24.workers.dev/health
225
+ - **Product Site:** https://scanbim.app
226
+ - **Company:** https://scanbimlabs.io
227
+ - **MCP Tools Page:** https://scanbimlabs.io/mcp
228
+
229
+ ---
230
+
231
+ ## License
232
+
233
+ MIT — Free for commercial use.
234
+
235
+ ---
236
+
237
+ **[ScanBIM Labs](https://scanbimlabs.io)** — VDC + AI + Reality Capture
238
+ *20 years of BIM/VDC operations, now AI-native. Built by a VDC practitioner, not a dev shop.*
@@ -0,0 +1,427 @@
1
+ // ACC MCP Worker v1.0.1 — APS-Backed ACC/BIM 360 Tools
2
+ // ScanBIM Labs LLC | Ian Martin
3
+
4
+ const APS_BASE = 'https://developer.api.autodesk.com';
5
+
6
+ const SERVER_INFO = {
7
+ name: "acc-mcp",
8
+ version: "1.0.1",
9
+ description: "Autodesk Construction Cloud integration via APS. Manage projects, issues, RFIs, documents, and submittals.",
10
+ author: "ScanBIM Labs LLC"
11
+ };
12
+
13
+ async function getAPSToken(env, scope = 'data:read data:write data:create') {
14
+ const cacheKey = `aps_token_${scope.replace(/\s/g,'_')}`;
15
+ if (env.CACHE) {
16
+ const cached = await env.CACHE.get(cacheKey);
17
+ if (cached) return cached;
18
+ }
19
+ const resp = await fetch(`${APS_BASE}/authentication/v2/token`, {
20
+ method: 'POST',
21
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
22
+ body: new URLSearchParams({
23
+ client_id: env.APS_CLIENT_ID,
24
+ client_secret: env.APS_CLIENT_SECRET,
25
+ grant_type: 'client_credentials',
26
+ scope
27
+ })
28
+ });
29
+ if (!resp.ok) throw new Error(`APS auth failed`);
30
+ const data = await resp.json();
31
+ const token = data.access_token;
32
+ if (env.CACHE) await env.CACHE.put(cacheKey, token, { expirationTtl: data.expires_in - 60 });
33
+ return token;
34
+ }
35
+
36
+ async function listHubs(token) {
37
+ const resp = await fetch(`${APS_BASE}/project/v1/hubs`, { headers: { Authorization: `Bearer ${token}` } });
38
+ if (!resp.ok) throw new Error(`List hubs failed`);
39
+ return await resp.json();
40
+ }
41
+
42
+ async function listProjects(token, hubId) {
43
+ const resp = await fetch(`${APS_BASE}/project/v1/hubs/${hubId}/projects`, { headers: { Authorization: `Bearer ${token}` } });
44
+ if (!resp.ok) throw new Error(`List projects failed`);
45
+ return await resp.json();
46
+ }
47
+
48
+ const TOOLS = [
49
+ { name: "acc_list_projects", description: "List all ACC/BIM 360 projects you have access to via APS Data Management", inputSchema: { type: "object", properties: {} } },
50
+ { name: "acc_create_issue", description: "Create a new issue in an ACC project via APS Issues API", inputSchema: { type: "object", properties: { project_id: { type: "string", description: "ACC project ID (b.xxxx format)" }, title: { type: "string" }, description: { type: "string" }, priority: { type: "string", enum: ["critical","high","medium","low"] }, assigned_to: { type: "string" }, due_date: { type: "string" } }, required: ["project_id", "title", "description"] } },
51
+ { name: "acc_update_issue", description: "Update an existing ACC issue (status, priority, assignee, description)", inputSchema: { type: "object", properties: { project_id: { type: "string" }, issue_id: { type: "string" }, status: { type: "string", enum: ["open","in_review","closed","draft"] }, priority: { type: "string", enum: ["critical","high","medium","low"] }, assigned_to: { type: "string" }, description: { type: "string" } }, required: ["project_id", "issue_id"] } },
52
+ { name: "acc_list_issues", description: "List and filter issues from an ACC project", inputSchema: { type: "object", properties: { project_id: { type: "string" }, status: { type: "string" }, priority: { type: "string" }, assigned_to: { type: "string" } }, required: ["project_id"] } },
53
+ { name: "acc_create_rfi", description: "Create a new RFI in an ACC project via APS RFIs API", inputSchema: { type: "object", properties: { project_id: { type: "string" }, subject: { type: "string" }, question: { type: "string" }, assigned_to: { type: "string" }, priority: { type: "string", enum: ["critical","high","medium","low"] } }, required: ["project_id", "subject", "question"] } },
54
+ { name: "acc_list_rfis", description: "List and filter RFIs from an ACC project", inputSchema: { type: "object", properties: { project_id: { type: "string" }, status: { type: "string" } }, required: ["project_id"] } },
55
+ { name: "acc_search_documents", description: "Search drawings, specs, submittals and documents in ACC via APS Data Management", inputSchema: { type: "object", properties: { project_id: { type: "string" }, query: { type: "string" }, document_type: { type: "string" } }, required: ["project_id", "query"] } },
56
+ { name: "acc_upload_file", description: "Upload a file to an ACC project folder via APS Data Management", inputSchema: { type: "object", properties: { project_id: { type: "string" }, file_url: { type: "string" }, file_name: { type: "string" }, folder_path: { type: "string" } }, required: ["project_id", "file_url", "file_name"] } },
57
+ { name: "acc_project_summary", description: "Get full ACC project summary including hub, metadata, issue counts, and RFI counts", inputSchema: { type: "object", properties: { project_id: { type: "string" }, hub_id: { type: "string" } }, required: ["project_id"] } }
58
+ ];
59
+
60
+ async function handleTool(name, args, env) {
61
+ if (env.DB) {
62
+ try { await env.DB.prepare("INSERT INTO usage_log (tool_name, model_id, created_at) VALUES (?, ?, ?)").bind(name, args.project_id || null, new Date().toISOString()).run(); } catch (e) {}
63
+ }
64
+
65
+ switch (name) {
66
+ case "acc_list_projects": {
67
+ const token = await getAPSToken(env, 'data:read');
68
+ const hubs = await listHubs(token);
69
+ const results = [];
70
+ for (const hub of (hubs.data || [])) {
71
+ const projects = await listProjects(token, hub.id);
72
+ for (const p of (projects.data || [])) {
73
+ results.push({ hub_id: hub.id, hub_name: hub.attributes?.name, project_id: p.id, project_name: p.attributes?.name, type: p.attributes?.extension?.type });
74
+ }
75
+ }
76
+ return { status: "success", project_count: results.length, projects: results };
77
+ }
78
+
79
+ case "acc_create_issue": {
80
+ const token = await getAPSToken(env, 'data:read data:write');
81
+ const cleanId = args.project_id.replace(/^b\./, '');
82
+ const resp = await fetch(`${APS_BASE}/construction/issues/v1/projects/${cleanId}/issues`, {
83
+ method: 'POST',
84
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({
86
+ title: args.title,
87
+ description: args.description,
88
+ status: 'open',
89
+ priority: args.priority || 'medium',
90
+ assignedTo: args.assigned_to || null,
91
+ dueDate: args.due_date || null
92
+ })
93
+ });
94
+ if (!resp.ok) throw new Error(`Create issue failed: ${await resp.text()}`);
95
+ const issue = await resp.json();
96
+ return { status: "success", issue_id: issue.data?.id || issue.id, title: args.title, priority: args.priority || 'medium', project_id: args.project_id };
97
+ }
98
+
99
+ case "acc_update_issue": {
100
+ const token = await getAPSToken(env, 'data:read data:write');
101
+ const cleanId = args.project_id.replace(/^b\./, '');
102
+ const updateBody = {};
103
+ if (args.status) updateBody.status = args.status;
104
+ if (args.priority) updateBody.priority = args.priority;
105
+ if (args.assigned_to) updateBody.assignedTo = args.assigned_to;
106
+ if (args.description) updateBody.description = args.description;
107
+ const resp = await fetch(`${APS_BASE}/construction/issues/v1/projects/${cleanId}/issues/${args.issue_id}`, {
108
+ method: 'PATCH',
109
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
110
+ body: JSON.stringify(updateBody)
111
+ });
112
+ if (!resp.ok) throw new Error(`Update issue failed: ${await resp.text()}`);
113
+ const issue = await resp.json();
114
+ return { status: "success", issue_id: args.issue_id, updated_fields: Object.keys(updateBody) };
115
+ }
116
+
117
+ case "acc_list_issues": {
118
+ const token = await getAPSToken(env, 'data:read');
119
+ const cleanId = args.project_id.replace(/^b\./, '');
120
+ let url = `${APS_BASE}/construction/issues/v1/projects/${cleanId}/issues?limit=50`;
121
+ if (args.status) url += `&filter[status]=${args.status}`;
122
+ if (args.priority) url += `&filter[priority]=${args.priority}`;
123
+ const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
124
+ if (!resp.ok) throw new Error(`List issues failed: ${await resp.text()}`);
125
+ const data = await resp.json();
126
+ const issues = (data.data || data.results || []).map(function(i) {
127
+ return {
128
+ id: i.id,
129
+ title: i.attributes ? i.attributes.title : i.title,
130
+ status: i.attributes ? i.attributes.status : i.status,
131
+ priority: i.attributes ? i.attributes.priority : i.priority,
132
+ due_date: i.attributes ? i.attributes.dueDate : i.due_date
133
+ };
134
+ });
135
+ return { status: "success", project_id: args.project_id, issue_count: issues.length, issues: issues };
136
+ }
137
+
138
+ case "acc_create_rfi": {
139
+ const token = await getAPSToken(env, 'data:read data:write');
140
+ const cleanId = args.project_id.replace(/^b\./, '');
141
+ const resp = await fetch(`${APS_BASE}/construction/rfis/v1/projects/${cleanId}/rfis`, {
142
+ method: 'POST',
143
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
144
+ body: JSON.stringify({
145
+ subject: args.subject,
146
+ question: args.question,
147
+ assignedTo: args.assigned_to || null,
148
+ priority: args.priority || 'medium',
149
+ status: 'draft'
150
+ })
151
+ });
152
+ if (!resp.ok) throw new Error(`Create RFI failed: ${await resp.text()}`);
153
+ const rfi = await resp.json();
154
+ return { status: "success", rfi_id: rfi.data?.id || rfi.id, subject: args.subject, project_id: args.project_id };
155
+ }
156
+
157
+ case "acc_list_rfis": {
158
+ const token = await getAPSToken(env, 'data:read');
159
+ const cleanId = args.project_id.replace(/^b\./, '');
160
+ let url = `${APS_BASE}/construction/rfis/v1/projects/${cleanId}/rfis?limit=50`;
161
+ if (args.status) url += `&filter[status]=${args.status}`;
162
+ const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
163
+ if (!resp.ok) throw new Error(`List RFIs failed: ${await resp.text()}`);
164
+ const data = await resp.json();
165
+ const rfis = (data.data || data.results || []).map(function(r) {
166
+ return {
167
+ id: r.id,
168
+ subject: r.attributes ? r.attributes.subject : r.subject,
169
+ status: r.attributes ? r.attributes.status : r.status
170
+ };
171
+ });
172
+ return { status: "success", project_id: args.project_id, rfi_count: rfis.length, rfis: rfis };
173
+ }
174
+
175
+ case "acc_search_documents": {
176
+ const token = await getAPSToken(env, 'data:read');
177
+ const cleanId = args.project_id.replace(/^b\./, '');
178
+ let url = `${APS_BASE}/data/v1/projects/b.${cleanId}/search?filter[text]=${encodeURIComponent(args.query)}`;
179
+ if (args.document_type) url += `&filter[type]=${args.document_type}`;
180
+ const resp = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
181
+ if (!resp.ok) throw new Error(`Document search failed: ${await resp.text()}`);
182
+ const data = await resp.json();
183
+ return { status: "success", project_id: args.project_id, query: args.query, results: data.data || [] };
184
+ }
185
+
186
+ case "acc_upload_file": {
187
+ const token = await getAPSToken(env, 'data:read data:write data:create');
188
+ const cleanId = args.project_id.replace(/^b\./, '');
189
+ const projectId = `b.${cleanId}`;
190
+ const folderPath = args.folder_path || "Project Files";
191
+
192
+ // Step 1: Get top-level folder for the project
193
+ const foldersResp = await fetch(
194
+ `${APS_BASE}/project/v1/hubs/b.${cleanId.split('.')[0] || cleanId}/projects/${projectId}/topFolders`,
195
+ { headers: { Authorization: `Bearer ${token}` } }
196
+ );
197
+
198
+ // Fallback: try listing hubs first to get the correct hub ID
199
+ let folderId = null;
200
+ if (foldersResp.ok) {
201
+ const foldersData = await foldersResp.json();
202
+ const targetFolder = (foldersData.data || []).find(function(f) {
203
+ const name = f.attributes?.displayName || f.attributes?.name || '';
204
+ return name.toLowerCase().includes(folderPath.toLowerCase());
205
+ });
206
+ if (targetFolder) folderId = targetFolder.id;
207
+
208
+ // If no match, use the first folder (usually "Project Files")
209
+ if (!folderId && foldersData.data && foldersData.data.length > 0) {
210
+ folderId = foldersData.data[0].id;
211
+ }
212
+ }
213
+
214
+ if (!folderId) {
215
+ // Try alternate hub discovery
216
+ const hubs = await listHubs(token);
217
+ for (const hub of (hubs.data || [])) {
218
+ const tfResp = await fetch(
219
+ `${APS_BASE}/project/v1/hubs/${hub.id}/projects/${projectId}/topFolders`,
220
+ { headers: { Authorization: `Bearer ${token}` } }
221
+ );
222
+ if (tfResp.ok) {
223
+ const tfData = await tfResp.json();
224
+ const match = (tfData.data || []).find(function(f) {
225
+ const name = f.attributes?.displayName || f.attributes?.name || '';
226
+ return name.toLowerCase().includes(folderPath.toLowerCase());
227
+ });
228
+ folderId = match ? match.id : (tfData.data?.[0]?.id || null);
229
+ if (folderId) break;
230
+ }
231
+ }
232
+ }
233
+
234
+ if (!folderId) {
235
+ return { status: "error", message: "Could not find target folder in project. Verify project_id and folder_path." };
236
+ }
237
+
238
+ // Step 2: Create storage location
239
+ const storageResp = await fetch(`${APS_BASE}/data/v1/projects/${projectId}/storage`, {
240
+ method: 'POST',
241
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/vnd.api+json' },
242
+ body: JSON.stringify({
243
+ jsonapi: { version: "1.0" },
244
+ data: {
245
+ type: "objects",
246
+ attributes: { name: args.file_name },
247
+ relationships: {
248
+ target: {
249
+ data: { type: "folders", id: folderId }
250
+ }
251
+ }
252
+ }
253
+ })
254
+ });
255
+
256
+ if (!storageResp.ok) {
257
+ const errText = await storageResp.text();
258
+ throw new Error(`Storage creation failed (${storageResp.status}): ${errText}`);
259
+ }
260
+
261
+ const storageData = await storageResp.json();
262
+ const objectId = storageData.data?.id;
263
+
264
+ if (!objectId) {
265
+ throw new Error("No storage object ID returned");
266
+ }
267
+
268
+ // Extract the signed upload URL from the storage object ID
269
+ // Format: urn:adsk.objects:os.object:wip.dm.prod/GUID
270
+ const bucketKey = objectId.split(':').pop().split('/')[0];
271
+ const objectKey = objectId.split('/').slice(1).join('/');
272
+ const uploadUrl = `${APS_BASE}/oss/v2/buckets/${bucketKey}/objects/${encodeURIComponent(objectKey)}`;
273
+
274
+ // Step 3: Fetch file from source URL and upload to APS storage
275
+ const fileResp = await fetch(args.file_url);
276
+ if (!fileResp.ok) {
277
+ throw new Error(`Cannot fetch source file from ${args.file_url}`);
278
+ }
279
+ const fileBytes = await fileResp.arrayBuffer();
280
+ const fileSizeMB = (fileBytes.byteLength / 1048576).toFixed(2);
281
+
282
+ const ossResp = await fetch(uploadUrl, {
283
+ method: 'PUT',
284
+ headers: {
285
+ Authorization: `Bearer ${token}`,
286
+ 'Content-Type': 'application/octet-stream',
287
+ 'Content-Length': fileBytes.byteLength.toString()
288
+ },
289
+ body: fileBytes
290
+ });
291
+
292
+ if (!ossResp.ok) {
293
+ const errText = await ossResp.text();
294
+ throw new Error(`File upload failed (${ossResp.status}): ${errText}`);
295
+ }
296
+
297
+ // Step 4: Create first version (item) in the folder
298
+ const itemResp = await fetch(`${APS_BASE}/data/v1/projects/${projectId}/items`, {
299
+ method: 'POST',
300
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/vnd.api+json' },
301
+ body: JSON.stringify({
302
+ jsonapi: { version: "1.0" },
303
+ data: {
304
+ type: "items",
305
+ attributes: {
306
+ displayName: args.file_name,
307
+ extension: {
308
+ type: "items:autodesk.bim360:File",
309
+ version: "1.0"
310
+ }
311
+ },
312
+ relationships: {
313
+ tip: {
314
+ data: { type: "versions", id: "1" }
315
+ },
316
+ parent: {
317
+ data: { type: "folders", id: folderId }
318
+ }
319
+ }
320
+ },
321
+ included: [{
322
+ type: "versions",
323
+ id: "1",
324
+ attributes: {
325
+ name: args.file_name,
326
+ extension: {
327
+ type: "versions:autodesk.bim360:File",
328
+ version: "1.0"
329
+ }
330
+ },
331
+ relationships: {
332
+ storage: {
333
+ data: { type: "objects", id: objectId }
334
+ }
335
+ }
336
+ }]
337
+ })
338
+ });
339
+
340
+ if (!itemResp.ok) {
341
+ const errText = await itemResp.text();
342
+ throw new Error(`Item creation failed (${itemResp.status}): ${errText}`);
343
+ }
344
+
345
+ const itemData = await itemResp.json();
346
+ const itemId = itemData.data?.id;
347
+ const versionId = itemData.included?.[0]?.id;
348
+
349
+ return {
350
+ status: "success",
351
+ project_id: args.project_id,
352
+ folder_id: folderId,
353
+ folder_path: folderPath,
354
+ file_name: args.file_name,
355
+ file_size_mb: fileSizeMB,
356
+ item_id: itemId,
357
+ version_id: versionId,
358
+ storage_object_id: objectId,
359
+ upload_status: "complete",
360
+ timestamp: new Date().toISOString()
361
+ };
362
+ }
363
+
364
+ case "acc_project_summary": {
365
+ const token = await getAPSToken(env, 'data:read');
366
+ const hubs = await listHubs(token);
367
+ const hubId = args.hub_id || (hubs.data && hubs.data[0] ? hubs.data[0].id : null);
368
+ if (!hubId) return { status: "error", message: "No hubs found" };
369
+ const resp = await fetch(`${APS_BASE}/project/v1/hubs/${hubId}/projects/${args.project_id}`, {
370
+ headers: { Authorization: `Bearer ${token}` }
371
+ });
372
+ if (!resp.ok) throw new Error(`Project summary failed: ${await resp.text()}`);
373
+ const summary = await resp.json();
374
+ return { status: "success", project: summary.data?.attributes || summary, hub_id: hubId };
375
+ }
376
+
377
+ default:
378
+ return { status: "error", message: "Unknown tool: " + name };
379
+ }
380
+ }
381
+
382
+ async function handleMCP(req, env) {
383
+ const body = await req.json();
384
+ var method = body.method;
385
+ var params = body.params;
386
+ var id = body.id;
387
+ var respond = function(result) { return new Response(JSON.stringify({ jsonrpc: "2.0", id: id, result: result }), { headers: { 'Content-Type': 'application/json' } }); };
388
+ var error = function(code, msg) { return new Response(JSON.stringify({ jsonrpc: "2.0", id: id, error: { code: code, message: msg } }), { headers: { 'Content-Type': 'application/json' } }); };
389
+
390
+ if (method === 'initialize') return respond({ protocolVersion: "2024-11-05", serverInfo: SERVER_INFO, capabilities: { tools: {} } });
391
+ if (method === 'tools/list') return respond({ tools: TOOLS });
392
+ if (method === 'tools/call') {
393
+ try {
394
+ var result = await handleTool(params.name, params.arguments || {}, env);
395
+ return respond({ content: [{ type: "text", text: JSON.stringify(result, null, 2) }] });
396
+ } catch (e) {
397
+ return respond({ content: [{ type: "text", text: JSON.stringify({ status: "error", message: e.message }) }] });
398
+ }
399
+ }
400
+ if (method === 'ping') return respond({});
401
+ return error(-32601, "Method not found");
402
+ }
403
+
404
+ export default {
405
+ async fetch(req, env) {
406
+ var url = new URL(req.url);
407
+ var cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type,Authorization' };
408
+
409
+ if (req.method === 'OPTIONS') return new Response(null, { headers: cors });
410
+
411
+ if (url.pathname === '/mcp' && req.method === 'POST') {
412
+ var resp = await handleMCP(req, env);
413
+ Object.entries(cors).forEach(function(e) { resp.headers.set(e[0], e[1]); });
414
+ return resp;
415
+ }
416
+
417
+ if (url.pathname === '/info' || url.pathname === '/') {
418
+ return new Response(JSON.stringify({ name: SERVER_INFO.name, version: SERVER_INFO.version, description: SERVER_INFO.description, tools_count: TOOLS.length }, null, 2), { headers: Object.assign({}, cors, { 'Content-Type': 'application/json' }) });
419
+ }
420
+
421
+ if (url.pathname === '/health') {
422
+ return new Response(JSON.stringify({ status: "ok", version: SERVER_INFO.version, aps_configured: !!(env.APS_CLIENT_ID && env.APS_CLIENT_SECRET) }), { headers: Object.assign({}, cors, { 'Content-Type': 'application/json' }) });
423
+ }
424
+
425
+ return new Response('ACC MCP v1.0.1 by ScanBIM Labs', { headers: cors });
426
+ }
427
+ };