Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .mcpbignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules/
.git/
.github/
.gitignore
rest-bridge.js
smart-bridge.js
index.js
tools.js
package-lock.json
*.md
236 changes: 7 additions & 229 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio
const { z } = require('zod');
const WebSocket = require('ws');

const { TOOLS, MCP_INSTRUCTIONS, registerMcpTools, registerMcpResources, registerMcpPrompts } = require('./tools.js');

const VERSION = require('./package.json').version;
const WS_PORT = 19802;
const REQUEST_TIMEOUT = 15000; // 15s for browser to respond

Expand Down Expand Up @@ -72,236 +75,11 @@ function sendToBrowser(action, params) {

// ── MCP Server ────────────────────────────────────────────────────

const server = new McpServer({
name: 'ClashControl',
version: '0.1.0',
});

// Helper: wrap a browser action as an MCP tool
function browserTool(name, description, schema, paramMapper) {
server.tool(name, description, schema, async (params) => {
try {
const mapped = paramMapper ? paramMapper(params) : params;
const result = await sendToBrowser(name, mapped);
return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] };
} catch (e) {
return { content: [{ type: 'text', text: 'Error: ' + e.message }], isError: true };
}
});
}

// ── Tool definitions ──────────────────────────────────────────────

// State queries (read-only)
server.tool('get_status', 'Get the current state of ClashControl: loaded models, clash count, active project, detection rules.', {}, async () => {
try {
const result = await sendToBrowser('get_status', {});
return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] };
} catch (e) {
return { content: [{ type: 'text', text: 'Error: ' + e.message }], isError: true };
}
});

server.tool('get_clashes', 'Get the current clash list with details (type, storey, status, elements involved). Returns up to 50 clashes.', {
status: z.enum(['open', 'resolved', 'all']).optional().describe('Filter by status'),
limit: z.number().optional().describe('Max clashes to return (default 50)'),
}, async (params) => {
try {
const result = await sendToBrowser('get_clashes', params);
return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] };
} catch (e) {
return { content: [{ type: 'text', text: 'Error: ' + e.message }], isError: true };
}
});

server.tool('get_issues', 'Get the current issues list with details.', {
limit: z.number().optional().describe('Max issues to return (default 50)'),
}, async (params) => {
try {
const result = await sendToBrowser('get_issues', params);
return { content: [{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2) }] };
} catch (e) {
return { content: [{ type: 'text', text: 'Error: ' + e.message }], isError: true };
}
});

// Detection
browserTool(
'run_detection',
'Run clash detection between two model groups. Pass model names, disciplines, or "all".',
{
modelA: z.string().describe('First side: model name, discipline, or "all". Use "+" to combine: "structural + architectural"'),
modelB: z.string().describe('Second side: model name, discipline, or "all"'),
maxGap: z.number().optional().describe('Gap tolerance in mm (default 10)'),
hard: z.boolean().optional().describe('true for hard/intersection clashes, false for soft/clearance'),
excludeSelf: z.boolean().optional().describe('Exclude self-clashes within same model'),
}
);

browserTool(
'set_detection_rules',
'Update clash detection settings without running detection.',
{
maxGap: z.number().optional().describe('Gap tolerance in mm'),
hard: z.boolean().optional().describe('Hard clash mode'),
excludeSelf: z.boolean().optional().describe('Exclude self-clashes'),
duplicates: z.boolean().optional().describe('Include duplicates'),
}
);

// Clash management
browserTool(
'update_clash',
'Update a specific clash: change status, priority, assignee, or title.',
{
clashIndex: z.number().describe('Clash index (0-based) in the current list'),
status: z.enum(['open', 'resolved']).optional(),
priority: z.enum(['critical', 'high', 'normal', 'low']).optional(),
assignee: z.string().optional(),
title: z.string().optional(),
}
);

browserTool(
'batch_update_clashes',
'Bulk update multiple clashes by filter.',
{
action: z.enum(['resolve', 'set_priority', 'set_status']).describe('Action to perform'),
filter: z.enum(['duplicates', 'soft', 'hard', 'all']).describe('Which clashes to target'),
value: z.string().optional().describe('New value for the action'),
}
);

// View controls
browserTool(
'set_view',
'Set the 3D camera to a preset angle.',
{
view: z.enum(['top', 'front', 'back', 'left', 'right', 'isometric', 'reset']).describe('Camera preset'),
}
);

browserTool(
'set_render_style',
'Change the 3D rendering style.',
{
style: z.enum(['wireframe', 'shaded', 'rendered', 'standard']).describe('Render style'),
}
);

browserTool(
'set_section',
'Add or clear a section cut plane.',
{
axis: z.enum(['x', 'y', 'z', 'none']).describe('Cut axis, or "none" to clear'),
}
);

browserTool(
'color_by',
'Color model elements by a property.',
{
by: z.enum(['type', 'storey', 'discipline', 'material', 'none']).describe('Color grouping'),
}
);

browserTool(
'set_theme',
'Switch UI theme.',
{ theme: z.enum(['dark', 'light']) }
);

browserTool(
'set_visibility',
'Show or hide UI overlays.',
{
option: z.enum(['grid', 'axes', 'markers']).describe('What to toggle'),
visible: z.boolean().describe('true to show, false to hide'),
}
);

browserTool(
'restore_visibility',
'Restore all hidden/ghosted/isolated elements to full visibility.',
{}
);

// Navigation
browserTool(
'fly_to_clash',
'Fly the camera to a specific clash by index.',
{ clashIndex: z.number().describe('Clash index (0-based)') }
);

browserTool(
'navigate_tab',
'Switch to a UI tab.',
{ tab: z.enum(['models', 'clashes', 'issues', 'navigator', 'ai']) }
);

// Filtering & sorting
browserTool(
'filter_clashes',
'Filter the clash list.',
{
status: z.enum(['open', 'resolved', 'all']).optional(),
priority: z.enum(['critical', 'high', 'normal', 'low', 'all']).optional(),
}
);

browserTool(
'sort_clashes',
'Sort the clash list.',
{
sortBy: z.enum(['priority', 'status', 'type', 'storey', 'date', 'distance']),
}
);

browserTool(
'group_clashes',
'Group clashes by a category.',
{
groupBy: z.enum(['storey', 'discipline', 'status', 'type', 'none']),
}
);

// Export
browserTool(
'export_bcf',
'Export clashes/issues as a BCF file (triggers download in browser).',
{
version: z.enum(['2.1', '3.0']).optional().describe('BCF version (default 2.1)'),
}
);

// Projects
browserTool(
'create_project',
'Create a new project.',
{ name: z.string().describe('Project name') }
);

browserTool(
'switch_project',
'Switch to an existing project by name.',
{ name: z.string().describe('Project name or substring') }
);

// Measurement
browserTool(
'measure',
'Start or stop measurement mode.',
{
mode: z.enum(['length', 'angle', 'area', 'stop', 'clear']).describe('Measurement mode'),
}
);
const server = new McpServer({ name: 'ClashControl', version: VERSION }, { instructions: MCP_INSTRUCTIONS });

// Walk mode
browserTool(
'walk_mode',
'Enter or exit first-person walk mode.',
{ enabled: z.boolean().describe('true to enter, false to exit') }
);
registerMcpTools(server, z, sendToBrowser);
registerMcpResources(server, sendToBrowser);
registerMcpPrompts(server, z);

// ── Start ─────────────────────────────────────────────────────────

Expand Down
48 changes: 48 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"manifest_version": "0.4",
"type": "mcp_server",
"name": "clashcontrol-mcp",
"display_name": "ClashControl — BIM Clash Detection",
"version": "0.2.0",
"description": "Control ClashControl BIM clash detection directly from Claude.",
"long_description": "Connect Claude to ClashControl, a browser-based IFC clash detection tool. Load IFC models, run clash detection between disciplines, review and triage clash pairs, navigate the 3D view, and export BCF reports — all through natural conversation. Requires ClashControl to be open in your browser with the Smart Bridge addon enabled.",
"authors": [
{ "name": "ClashControl", "url": "https://clashcontrol.io" }
],
"repository": "https://github.com/clashcontrol-io/ClashControlSmartBridge",
"server": {
"type": "node",
"entry_point": "dist/mcp-bundle.cjs"
},
"compatibility": {
"platforms": ["darwin", "win32"],
"runtimes": { "node": ">=18.0.0" }
},
"tools": [
{ "name": "get_status", "description": "Get loaded models, clash count, project, detection rules" },
{ "name": "get_clashes", "description": "Get clash list with details" },
{ "name": "get_issues", "description": "Get issues list" },
{ "name": "run_detection", "description": "Run clash detection between model groups" },
{ "name": "set_detection_rules", "description": "Update detection settings" },
{ "name": "update_clash", "description": "Update a specific clash" },
{ "name": "batch_update_clashes", "description": "Bulk update clashes by filter" },
{ "name": "set_view", "description": "Set 3D camera preset" },
{ "name": "set_render_style", "description": "Change rendering style" },
{ "name": "set_section", "description": "Add or clear section cut plane" },
{ "name": "color_by", "description": "Color elements by property" },
{ "name": "set_theme", "description": "Switch UI theme" },
{ "name": "set_visibility", "description": "Show or hide UI overlays" },
{ "name": "restore_visibility", "description": "Restore all hidden elements" },
{ "name": "fly_to_clash", "description": "Fly camera to a clash" },
{ "name": "navigate_tab", "description": "Switch UI tab" },
{ "name": "filter_clashes", "description": "Filter clash list" },
{ "name": "sort_clashes", "description": "Sort clash list" },
{ "name": "group_clashes", "description": "Group clashes by category" },
{ "name": "export_bcf", "description": "Export clashes as BCF" },
{ "name": "create_project", "description": "Create a new project" },
{ "name": "switch_project", "description": "Switch to a project" },
{ "name": "measure", "description": "Start or stop measurement mode" },
{ "name": "walk_mode", "description": "Enter or exit walk mode" }
],
"tools_generated": false
}
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@clashcontrol/mcp-server",
"version": "0.1.6",
"version": "0.2.0",
"description": "ClashControl Smart Bridge — LLM bridge connecting Claude, ChatGPT, or any AI assistant to BIM clash detection",
"main": "smart-bridge.js",
"bin": {
Expand All @@ -9,12 +9,14 @@
"scripts": {
"start": "node smart-bridge.js",
"mcp": "node smart-bridge.js --mcp",
"build:mcp": "npx esbuild index.js --bundle --platform=node --target=node18 --format=cjs --outfile=dist/mcp-bundle.cjs",
"build:pkg": "npx pkg smart-bridge.js --targets node18-win-x64,node18-macos-x64,node18-linux-x64 --output dist/clashcontrol-smart-bridge",
"bundle": "npx pkg smart-bridge.js --targets node18-win-x64,node18-macos-x64,node18-linux-x64 --output dist/clashcontrol-smart-bridge"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.12.1",
"@modelcontextprotocol/sdk": "^1.29.0",
"ws": "^8.18.0",
"zod": "^3.23.0"
"zod": "^3.25.0"
},
"pkg": {
"assets": [
Expand Down
Loading
Loading