@rui.branco/revit-mcp 1.0.0

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.
@@ -0,0 +1,208 @@
1
+ // Installing the bridge add-in. This is the one pair of tools that must work
2
+ // with Revit closed and the bridge absent — it is how the bridge gets there in
3
+ // the first place — so nothing here touches lib/bridge.js or the HTTP port.
4
+ //
5
+ // The work is done by revit-bridge/install.ps1, which detects Revit and copies
6
+ // the add-in into the Addins folder. The add-in ships prebuilt in
7
+ // revit-bridge/dist/, so the usual path needs no .NET SDK and no build; only a
8
+ // source checkout with no dist/ falls back to `dotnet build -c Release`. We run
9
+ // it with -Json, which makes it print one JSON object and nothing else.
10
+
11
+ import { spawn } from "child_process";
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ import { z } from "zod";
15
+
16
+ // Resolved from this module, never from process.cwd(): an MCP server is
17
+ // started by the client from whatever directory it likes.
18
+ export const INSTALL_SCRIPT = path.resolve(
19
+ path.dirname(fileURLToPath(import.meta.url)),
20
+ "..",
21
+ "..",
22
+ "revit-bridge",
23
+ "install.ps1",
24
+ );
25
+
26
+ // Installing the bundled add-in is a file copy, but the source-checkout
27
+ // fallback still shells out to dotnet build, which on a cold NuGet cache is
28
+ // minutes, not seconds.
29
+ export const INSTALL_TIMEOUT_MS = 300000;
30
+
31
+ // PowerShell 7 if it is there, Windows PowerShell otherwise — the script is
32
+ // written to run under both.
33
+ const SHELLS = ["pwsh.exe", "powershell.exe"];
34
+
35
+ // install.ps1 exits with a code per failure mode, so none of them has to be
36
+ // guessed at from the prose.
37
+ export function explainExitCode(code, action = "install") {
38
+ switch (code) {
39
+ case 0:
40
+ if (action === "uninstall") return "Success: the add-in was removed. Restart Revit to unload it.";
41
+ return "Success: the add-in is installed. Revit must be RESTARTED before the bridge works — Revit only scans the Addins folder at startup.";
42
+ case 1:
43
+ return "The installer hit an unhandled failure. The `errors` and `log` fields say what threw.";
44
+ case 2:
45
+ if (action === "uninstall")
46
+ return "Nothing to uninstall: no Revit installation and no Revit Addins folder was found, so there was nothing to remove. Not a failure.";
47
+ return "No Revit installation was found under Program Files\\Autodesk. Install Revit, or pass revit_version to target a version installed somewhere else.";
48
+ case 3:
49
+ return "No supported Revit version. This add-in is .NET 8 and needs Revit 2025 or newer; Revit 2024 and earlier load add-ins on .NET Framework 4.8 and cannot run it. A revit_version that is not a four-digit year also lands here.";
50
+ case 4:
51
+ return "`dotnet build` failed, or the .NET SDK is not installed, so nothing was installed. This only happens in a source checkout with no prebuilt revit-bridge/dist/ — the published package ships one. The build output is in `log`.";
52
+ case 5:
53
+ return "Nothing to install: no prebuilt add-in in revit-bridge/dist/ and no Release build output either. Re-run without skip_build so the add-in gets built, or reinstall the package to restore the bundled binary.";
54
+ default:
55
+ return `install.ps1 exited with code ${code}.`;
56
+ }
57
+ }
58
+
59
+ // Argv, never a command string: a path with a space in it must not become two
60
+ // arguments, and nothing here may be shell-interpreted.
61
+ export function installerArgv(
62
+ { uninstall = false, revitVersion, skipBuild } = {},
63
+ scriptPath = INSTALL_SCRIPT,
64
+ ) {
65
+ const argv = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath];
66
+ if (uninstall) argv.push("-Uninstall");
67
+ if (revitVersion) argv.push("-RevitVersion", revitVersion);
68
+ if (skipBuild) argv.push("-SkipBuild");
69
+ argv.push("-Json");
70
+ return argv;
71
+ }
72
+
73
+ function runShell(exe, argv, spawnImpl, timeoutMs) {
74
+ return new Promise((resolve, reject) => {
75
+ const child = spawnImpl(exe, argv, { windowsHide: true });
76
+ let stdout = "";
77
+ let stderr = "";
78
+ let timedOut = false;
79
+
80
+ // Stop waiting rather than waiting for the kill to land: a dotnet build
81
+ // that outlives the timeout must not hold the tool call open too.
82
+ const timer = setTimeout(() => {
83
+ timedOut = true;
84
+ child.kill();
85
+ resolve({ exe, code: null, stdout, stderr, timedOut });
86
+ }, timeoutMs);
87
+
88
+ child.stdout.setEncoding("utf8");
89
+ child.stderr.setEncoding("utf8");
90
+ child.stdout.on("data", (chunk) => (stdout += chunk));
91
+ child.stderr.on("data", (chunk) => (stderr += chunk));
92
+
93
+ child.on("error", (err) => {
94
+ clearTimeout(timer);
95
+ reject(err);
96
+ });
97
+ child.on("close", (code) => {
98
+ clearTimeout(timer);
99
+ resolve({ exe, code, stdout, stderr, timedOut });
100
+ });
101
+ });
102
+ }
103
+
104
+ async function runAnyShell(argv, spawnImpl, timeoutMs) {
105
+ let lastError;
106
+ for (const exe of SHELLS) {
107
+ try {
108
+ return await runShell(exe, argv, spawnImpl, timeoutMs);
109
+ } catch (err) {
110
+ // Only "that executable does not exist" is worth falling back on.
111
+ if (err.code !== "ENOENT") throw err;
112
+ lastError = err;
113
+ }
114
+ }
115
+ throw new Error(
116
+ `Could not start PowerShell: neither ${SHELLS.join(" nor ")} could be launched (${lastError.message}). The Revit bridge only installs on Windows.`,
117
+ );
118
+ }
119
+
120
+ // -Json promises a single JSON object as the only stdout. Anything else means
121
+ // the script died before it got there, and the raw output is the evidence.
122
+ function parseResult(stdout) {
123
+ try {
124
+ const parsed = JSON.parse(stdout.trim());
125
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
126
+ return parsed;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ export async function runInstaller(
133
+ options = {},
134
+ { spawnImpl = spawn, scriptPath = INSTALL_SCRIPT, timeoutMs = INSTALL_TIMEOUT_MS } = {},
135
+ ) {
136
+ const action = options.uninstall ? "uninstall" : "install";
137
+ const run = await runAnyShell(installerArgv(options, scriptPath), spawnImpl, timeoutMs);
138
+
139
+ if (run.timedOut) {
140
+ return {
141
+ ok: false,
142
+ action,
143
+ timedOut: true,
144
+ timeoutMs,
145
+ message: `install.ps1 has not finished after ${timeoutMs}ms. This is not a failure: 'dotnet build' can take longer than that on a cold NuGet cache, and the build it started may still be finishing. Wait a minute and run this tool again — re-running is safe — or check ${scriptPath} manually.`,
146
+ stdout: run.stdout,
147
+ stderr: run.stderr,
148
+ };
149
+ }
150
+
151
+ const parsed = parseResult(run.stdout);
152
+
153
+ if (!parsed) {
154
+ return {
155
+ ok: false,
156
+ action,
157
+ exitCode: run.code,
158
+ explanation: explainExitCode(run.code, action),
159
+ message: "install.ps1 did not print the JSON object -Json promises. Its raw output follows.",
160
+ stdout: run.stdout,
161
+ stderr: run.stderr,
162
+ };
163
+ }
164
+
165
+ return { ...parsed, exitCode: run.code, explanation: explainExitCode(run.code, action) };
166
+ }
167
+
168
+ export function registerInstallTools(server, options = {}) {
169
+ server.tool(
170
+ "revit_install_bridge",
171
+ "Install the Revit MCP bridge add-in: copies the add-in that ships with this package into Revit's Addins folder. No .NET SDK and no build step are involved. Use this when revit_status says nothing is listening, or on a machine where the bridge has never been set up. Revit does NOT need to be running. RESTART REVIT after this succeeds — Revit only scans the Addins folder at startup, so the bridge does not load until Revit is restarted. Re-running is safe.",
172
+ {
173
+ revit_version: z
174
+ .string()
175
+ .optional()
176
+ .describe("Four-digit Revit year, e.g. '2026'. Omit to install into every Revit found on this machine."),
177
+ skip_build: z
178
+ .boolean()
179
+ .optional()
180
+ .describe("Never build, even in a source checkout with no prebuilt add-in. A no-op for the published package, which always installs its bundled binary without building."),
181
+ },
182
+ async ({ revit_version, skip_build }) => {
183
+ try {
184
+ const result = await runInstaller(
185
+ { revitVersion: revit_version, skipBuild: skip_build },
186
+ options,
187
+ );
188
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
189
+ } catch (error) {
190
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
191
+ }
192
+ },
193
+ );
194
+
195
+ server.tool(
196
+ "revit_uninstall_bridge",
197
+ "Remove the Revit MCP bridge add-in: deletes the .addin manifest and the install folder for every Revit version that has it. Revit does NOT need to be running, but RESTART REVIT afterwards — a running Revit keeps the already-loaded bridge alive until it closes.",
198
+ {},
199
+ async () => {
200
+ try {
201
+ const result = await runInstaller({ uninstall: true }, options);
202
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
203
+ } catch (error) {
204
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
205
+ }
206
+ },
207
+ );
208
+ }
@@ -0,0 +1,168 @@
1
+ // The rendered look behind a material: its appearance asset.
2
+ //
3
+ // revit_create_material and revit_set_material_texture author a look. These two
4
+ // read and edit the one a material already has, which is what "this stucco is
5
+ // too pale" and "take the orange out of the window frames" actually need.
6
+ //
7
+ // An appearance asset has no fixed set of properties: what it carries depends
8
+ // on the SCHEMA it was built from — a Generic asset has "generic_diffuse",
9
+ // "generic_glossiness" and "generic_transparency"; a Ceramic one has
10
+ // "ceramic_color"; a Water one has neither. So there is no guessing here:
11
+ // revit_get_material_appearance lists what the asset really has, with the type
12
+ // of each property, and revit_set_material_appearance writes the properties you
13
+ // name, by the type you name, or fails the whole call.
14
+ //
15
+ // Two rules the bridge does not bend, and they are worth knowing before you
16
+ // call it:
17
+ //
18
+ // - The asset is DUPLICATED before it is patched. Two materials very often
19
+ // share one appearance asset, and editing it in place repaints both.
20
+ // - A patch that does not fit — an unknown property, the wrong type, a value
21
+ // Revit's own IsValidValue refuses — rolls the entire request back. It
22
+ // never reports success for a change that did not happen.
23
+ //
24
+ // That second rule is why both catches answer with `isError: true`: a rolled
25
+ // back patch reaches here as a bridge exception, and without the flag an MCP
26
+ // client reads it as a successful call whose text happens to start with
27
+ // "Error:".
28
+
29
+ import { z } from "zod";
30
+
31
+ const CHANNEL = z.number().int().min(0).max(255);
32
+
33
+ const RGB = z.object({ r: CHANNEL, g: CHANNEL, b: CHANNEL });
34
+
35
+ // Named the same way as every other material tool: material_id wins, and
36
+ // material_name is the alternative. Neither is required here — the bridge is
37
+ // what says so, with the message that names the endpoint to call instead.
38
+ const MATERIAL = {
39
+ material_id: z
40
+ .number()
41
+ .int()
42
+ .optional()
43
+ .describe("Material id from revit_list_materials or revit_create_material"),
44
+ material_name: z
45
+ .string()
46
+ .min(1)
47
+ .optional()
48
+ .describe("Material name, as an alternative to material_id. Must already exist in the document."),
49
+ };
50
+
51
+ export function registerMaterialAppearanceTools(server, bridge) {
52
+ server.tool(
53
+ "revit_get_material_appearance",
54
+ "Read the appearance asset behind a material — the rendered look, as opposed to the shading colour revit_create_material sets. Read-only: it changes nothing. It reports the material's shading side (colorRgb, transparency, shininess, smoothness, useRenderAppearanceForShading), the AppearanceAssetElement it really points at (id, name and the SCHEMA it was built from, e.g. 'Generic' or 'Ceramic'), and every direct property of that asset: 'name' as the API knows it (e.g. 'generic_diffuse'), 'type' and 'runtimeType', the typed 'value' it holds, and 'patchType' — which of revit_set_material_appearance's five types can write it, or null when none can. Anything connected to a property comes back under 'connected' with the bitmap's file, tile size in feet and rotation. 'sharedWithMaterialIds' is every OTHER material pointing at the same asset: non-empty means editing it in place would repaint them too, which is exactly what revit_set_material_appearance refuses to do. NOTE on 'readOnly': Revit hands the rendering asset out read-only outside an edit scope, so it is usually true for every property and is NOT the test of whether a property can be written — revit_set_material_appearance is, because it validates inside an edit scope. A material with no appearance asset at all is a normal state, not a broken one: it renders from Color and Transparency alone, appearanceAssetId comes back null with an empty property list, and it does NOT need a bitmap — create_generic on revit_set_material_appearance gives it a textureless Generic asset to patch. 'genericAssetAvailable' says up front whether Revit's library can supply one on this machine.",
55
+ MATERIAL,
56
+ async ({ material_id, material_name }) => {
57
+ try {
58
+ const result = await bridge.call("/materials/appearance", {
59
+ materialId: material_id,
60
+ materialName: material_name,
61
+ });
62
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
63
+ } catch (error) {
64
+ return {
65
+ content: [{ type: "text", text: `Error: ${error.message}` }],
66
+ isError: true,
67
+ };
68
+ }
69
+ },
70
+ );
71
+
72
+ server.tool(
73
+ "revit_set_material_appearance",
74
+ "Edit the appearance asset behind a material by naming the properties to write. Call revit_get_material_appearance FIRST: every patch names a property of the asset's own schema ('generic_diffuse', 'generic_glossiness', ...), and a name that schema does not have fails the call instead of quietly doing nothing. Each patch is {name, type, value} where type is 'color', 'double', 'integer', 'boolean' or 'string' — the type is checked against the property's real runtime type and the value against Revit's own IsValidValue, and one patch that does not fit rolls the WHOLE request back. Colours are {r, g, b} with each channel 0-255, which Revit stores inside the asset as doubles 0-1; both come back in the readback. The asset is DUPLICATED before it is patched (duplicate defaults to true), because materials very often share one and editing it in place would repaint every one of them — 'duplicate': false is refused with SHARED_APPEARANCE_ASSET when anybody else points at it, and there is deliberately no way to force a shared edit. 'sharedWithMaterialIds' in the reply is computed against the RESULTING asset, so an empty list is the evidence nothing leaked. source_appearance_asset_id duplicates another material's asset onto this one (the source keeps its look). create_generic gives a material with NO appearance asset a textureless Generic asset built from Revit's own library — a null appearance asset never means the material needs a bitmap — and answers GENERIC_ASSET_UNAVAILABLE when the material libraries are not installed. disconnect_texture only matters for a 'color' patch and only when explicitly true: a colour written under a connected bitmap renders as nothing, so that patch is refused unless you say to remove the texture. sync_shading_color also copies the first patched colour onto the material's own Color, which is what SHADED views draw when useRenderAppearanceForShading is false; otherwise the shading colour the material had is preserved, even when Revit would have repainted it to match a new asset. The reply carries 'applied', 'verified' (the committed asset read back), the resulting appearanceAssetId and the asset it replaced. One call is one undo step.",
75
+ {
76
+ ...MATERIAL,
77
+ patches: z
78
+ .array(
79
+ z.object({
80
+ name: z
81
+ .string()
82
+ .min(1)
83
+ .describe(
84
+ "Asset property name, exactly as revit_get_material_appearance lists it, e.g. 'generic_diffuse'",
85
+ ),
86
+ type: z
87
+ .enum(["color", "double", "integer", "boolean", "string"])
88
+ .describe(
89
+ "Type to write it as. It must match the property's 'patchType' from revit_get_material_appearance.",
90
+ ),
91
+ value: z
92
+ .union([RGB, z.number(), z.boolean(), z.string()])
93
+ .describe(
94
+ "The value: {r, g, b} 0-255 for 'color', a number for 'double' and 'integer', true/false for 'boolean', text for 'string'",
95
+ ),
96
+ }),
97
+ )
98
+ .optional()
99
+ .describe(
100
+ "Properties to write, all in this one call. Optional only when create_generic or source_appearance_asset_id is given; otherwise the call has nothing to do and is refused.",
101
+ ),
102
+ duplicate: z
103
+ .boolean()
104
+ .optional()
105
+ .describe(
106
+ "Duplicate the asset before patching it. Defaults to true. False edits in place and is refused when another material shares the asset.",
107
+ ),
108
+ source_appearance_asset_id: z
109
+ .number()
110
+ .int()
111
+ .optional()
112
+ .describe(
113
+ "Id of an appearance asset to start from, from revit_list_materials or revit_get_material_appearance. It is duplicated and the copy assigned to THIS material only, so the material it came from is untouched.",
114
+ ),
115
+ create_generic: z
116
+ .boolean()
117
+ .optional()
118
+ .describe(
119
+ "Give the material a new textureless Generic appearance asset from Revit's library and patch that. This is the route for a material whose appearanceAssetId is null.",
120
+ ),
121
+ disconnect_texture: z
122
+ .boolean()
123
+ .optional()
124
+ .describe(
125
+ "Remove a bitmap connected to a property being patched as a colour, so the colour is what renders. Only applies to 'color' patches, and only when explicitly true.",
126
+ ),
127
+ sync_shading_color: z
128
+ .boolean()
129
+ .optional()
130
+ .describe(
131
+ "Also write the first patched colour onto the material's own Color, which is what shaded views draw. Needs at least one 'color' patch.",
132
+ ),
133
+ },
134
+ async ({
135
+ material_id,
136
+ material_name,
137
+ patches,
138
+ duplicate,
139
+ source_appearance_asset_id,
140
+ create_generic,
141
+ disconnect_texture,
142
+ sync_shading_color,
143
+ }) => {
144
+ try {
145
+ // The tool arguments stay snake_case like every other tool's; the bridge
146
+ // reads materialId / sourceAppearanceAssetId / ... — map them here rather
147
+ // than on the C# side. A patch keeps its {name, type, value} shape on the
148
+ // wire, because those three are the contract on both sides.
149
+ const result = await bridge.call("/materials/set-appearance", {
150
+ materialId: material_id,
151
+ materialName: material_name,
152
+ patches,
153
+ duplicate,
154
+ sourceAppearanceAssetId: source_appearance_asset_id,
155
+ createGeneric: create_generic,
156
+ disconnectTexture: disconnect_texture,
157
+ syncShadingColor: sync_shading_color,
158
+ });
159
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
160
+ } catch (error) {
161
+ return {
162
+ content: [{ type: "text", text: `Error: ${error.message}` }],
163
+ isError: true,
164
+ };
165
+ }
166
+ },
167
+ );
168
+ }
@@ -0,0 +1,247 @@
1
+ // Materials, and the wall and floor types that carry one.
2
+ //
3
+ // Nothing else here set a material, so everything the bridge built rendered in
4
+ // Revit's default grey. There are two routes onto an element and they are not
5
+ // interchangeable:
6
+ //
7
+ // - Geometry built by revit_create_directshape, revit_place_planting,
8
+ // revit_create_pipes and revit_place_sprinklers carries its material on the
9
+ // solid. That is decided when the solid is built, so those tools take a
10
+ // material_id and revit_assign_material cannot help them afterwards.
11
+ // - A wall or a floor takes its material from its TYPE's compound structure:
12
+ // revit_create_wall_type / revit_create_floor_type author one, and
13
+ // revit_create_walls / revit_create_floor build with it by name.
14
+ //
15
+ // revit_assign_material covers what is left: elements with a real material
16
+ // parameter, and walls/floors whose type it can edit. It says per element which
17
+ // route it used, and which elements could take neither.
18
+ //
19
+ // Colour channels are 0-255; transparency is 0-100 and shininess 0-128, which
20
+ // are Revit's own ranges. Thickness is feet, like every other length here.
21
+ //
22
+ // A colour on its own is still flat paint. revit_set_material_texture is what
23
+ // puts a real bitmap on a material — grass that looks like grass rather than
24
+ // like green — and Revit ships the bitmaps to do it with, under
25
+ // C:\Program Files\Common Files\Autodesk Shared\Materials\Textures.
26
+
27
+ import { z } from "zod";
28
+
29
+ const MATERIAL_CHANNEL = z.number().int().min(0).max(255);
30
+
31
+ // Shared by the two type tools: they differ only in which type kind they make.
32
+ const HOST_TYPE = {
33
+ name: z.string().min(1).describe("Name for the new type. Must be unique among types of this kind."),
34
+ based_on_type_name: z
35
+ .string()
36
+ .min(1)
37
+ .optional()
38
+ .describe("Existing type to duplicate. Omit it to duplicate the document's default type."),
39
+ thickness: z
40
+ .number()
41
+ .positive()
42
+ .optional()
43
+ .describe("Thickness of the single structural layer, in feet. Omit it to keep the source type's."),
44
+ material_id: z
45
+ .number()
46
+ .int()
47
+ .optional()
48
+ .describe("Material id for that layer, from revit_list_materials or revit_create_material."),
49
+ material_name: z
50
+ .string()
51
+ .min(1)
52
+ .optional()
53
+ .describe("Material name, as an alternative to material_id. Must already exist in the document."),
54
+ };
55
+
56
+ export function registerMaterialTools(server, bridge) {
57
+ server.tool(
58
+ "revit_list_materials",
59
+ "List the materials in the document: id, name, colour as {r, g, b}, and the id of its appearance asset when it has one. This is where a material_id comes from — for revit_create_directshape, revit_place_planting, revit_create_pipes, revit_place_sprinklers, revit_assign_material, revit_create_wall_type and revit_create_floor_type. A Revit template usually ships dozens, so check here before creating a new one.",
60
+ {},
61
+ async () => {
62
+ try {
63
+ const result = await bridge.call("/materials", {});
64
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
65
+ } catch (error) {
66
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
67
+ }
68
+ },
69
+ );
70
+
71
+ server.tool(
72
+ "revit_create_material",
73
+ "Create a material with a colour, so the model stops rendering in Revit's default grey. 'color' is {r, g, b}, each 0-255. 'transparency' is 0-100 (0 is opaque — use it for water and glass) and 'shininess' is 0-128; omitted, Revit's own defaults are left alone. A material that already exists by this name is REUSED and comes back with created:false and its current properties — nothing about it is overwritten, so re-running the same call is safe, and a colour you did not ask for means somebody else authored that material. Shading is set to follow the colour rather than a render appearance, which is what makes the colour visible in a shaded view. 'appearance_asset_id' takes the look of an existing material's appearance asset (see revit_list_materials): it is duplicated, not shared. 'texture_path' puts a real bitmap on it in the same call — what that did comes back under 'texture'; it is ignored for a material that already exists, which revit_set_material_texture can texture instead.",
74
+ {
75
+ name: z.string().min(1).describe("Material name, e.g. 'Relva' or 'Betão pigmentado'"),
76
+ color: z
77
+ .object({ r: MATERIAL_CHANNEL, g: MATERIAL_CHANNEL, b: MATERIAL_CHANNEL })
78
+ .describe("Colour as {r, g, b}, each channel 0-255"),
79
+ transparency: z
80
+ .number()
81
+ .int()
82
+ .min(0)
83
+ .max(100)
84
+ .optional()
85
+ .describe("Transparency 0-100; 0 is opaque. Omit it to keep Revit's default."),
86
+ shininess: z
87
+ .number()
88
+ .int()
89
+ .min(0)
90
+ .max(128)
91
+ .optional()
92
+ .describe("Shininess 0-128. Omit it to keep Revit's default."),
93
+ surface_foreground_pattern_id: z
94
+ .number()
95
+ .int()
96
+ .optional()
97
+ .describe("Id of a FillPatternElement to use as the surface foreground pattern"),
98
+ appearance_asset_id: z
99
+ .number()
100
+ .int()
101
+ .optional()
102
+ .describe(
103
+ "Id of an AppearanceAssetElement to copy the rendered look of, from revit_list_materials. It is duplicated so later edits cannot leak between materials.",
104
+ ),
105
+ texture_path: z
106
+ .string()
107
+ .min(1)
108
+ .optional()
109
+ .describe(
110
+ "Full path to a texture bitmap, to make the material textured in this same call. Revit's own library is under C:\\Program Files\\Common Files\\Autodesk Shared\\Materials\\Textures. A file that does not exist is refused before anything is created. Tile size, rotation and tint belong to revit_set_material_texture.",
111
+ ),
112
+ },
113
+ async ({
114
+ name,
115
+ color,
116
+ transparency,
117
+ shininess,
118
+ surface_foreground_pattern_id,
119
+ appearance_asset_id,
120
+ texture_path,
121
+ }) => {
122
+ try {
123
+ const result = await bridge.call("/materials/create", {
124
+ name,
125
+ color,
126
+ transparency,
127
+ shininess,
128
+ surfaceForegroundPatternId: surface_foreground_pattern_id,
129
+ appearanceAssetId: appearance_asset_id,
130
+ texturePath: texture_path,
131
+ });
132
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
133
+ } catch (error) {
134
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
135
+ }
136
+ },
137
+ );
138
+
139
+ server.tool(
140
+ "revit_set_material_texture",
141
+ "Put a real texture bitmap on an existing material, which is the difference between a surface that is green and one that looks like grass. Revit ships thousands of bitmaps under C:\\Program Files\\Common Files\\Autodesk Shared\\Materials\\Textures — pass a full path to one of those, or to any image file this machine can read; it is REFERENCED by the model, not copied into it, so a path that stops existing is a texture that stops rendering. A file that is not there is refused with TEXTURE_NOT_FOUND. 'scale' is the real-world size of one tile of the bitmap, {x, y} in feet — 3 by 3 makes a paving texture read as 3-foot slabs. 'rotation' is degrees and 'tint' is a colour multiplied over the bitmap. The material is given its own Generic appearance asset unless it already has one nobody else shares, so texturing one material can never change another's look; the reply says whether that happened and why. It reports every asset property it wrote under 'set', anything the asset turned out not to have under 'missing', and what the saved asset reads back as under 'verified'. One call is one undo step.",
142
+ {
143
+ material_id: z
144
+ .number()
145
+ .int()
146
+ .optional()
147
+ .describe("Material id from revit_list_materials or revit_create_material"),
148
+ material_name: z
149
+ .string()
150
+ .min(1)
151
+ .optional()
152
+ .describe("Material name, as an alternative to material_id. Must already exist in the document."),
153
+ texture_path: z
154
+ .string()
155
+ .min(1)
156
+ .describe(
157
+ "Full path to the texture bitmap, e.g. 'C:\\Program Files\\Common Files\\Autodesk Shared\\Materials\\Textures\\1\\Mats\\grass_color.jpg'",
158
+ ),
159
+ scale: z
160
+ .object({ x: z.number().positive(), y: z.number().positive() })
161
+ .optional()
162
+ .describe(
163
+ "Real-world size of one tile of the bitmap, {x, y} in feet. Omit it to leave Revit's own tile size.",
164
+ ),
165
+ rotation: z.number().optional().describe("Rotation of the texture, in degrees"),
166
+ tint: z
167
+ .object({ r: MATERIAL_CHANNEL, g: MATERIAL_CHANNEL, b: MATERIAL_CHANNEL })
168
+ .optional()
169
+ .describe("Colour multiplied over the bitmap, as {r, g, b}, each channel 0-255"),
170
+ },
171
+ async ({ material_id, material_name, texture_path, scale, rotation, tint }) => {
172
+ try {
173
+ const result = await bridge.call("/materials/set-texture", {
174
+ materialId: material_id,
175
+ materialName: material_name,
176
+ texturePath: texture_path,
177
+ scale,
178
+ rotation,
179
+ tint,
180
+ });
181
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
182
+ } catch (error) {
183
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
184
+ }
185
+ },
186
+ );
187
+
188
+ server.tool(
189
+ "revit_assign_material",
190
+ "Put an existing material on elements that already exist. Two routes, chosen per element and reported per element: a wall, floor, roof, ceiling or toposolid gets it through its TYPE's compound structure — which changes every element of that type, and the row says so with appliesToType — and anything with a writable material parameter gets it written there. Elements that can take neither come back under 'skipped' with the reason. DirectShape elements (everything from revit_create_directshape, revit_place_planting, revit_create_pipes and revit_place_sprinklers) are always skipped: their material is carried by the solid and fixed when it is built, so pass material_id to those tools instead. One call is one undo step.",
191
+ {
192
+ material_id: z.number().int().describe("Material id from revit_list_materials or revit_create_material"),
193
+ element_ids: z.array(z.number().int()).min(1).describe("Elements to put the material on"),
194
+ },
195
+ async ({ material_id, element_ids }) => {
196
+ try {
197
+ const result = await bridge.call("/materials/assign", {
198
+ materialId: material_id,
199
+ elementIds: element_ids,
200
+ });
201
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
202
+ } catch (error) {
203
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
204
+ }
205
+ },
206
+ );
207
+
208
+ server.tool(
209
+ "revit_create_wall_type",
210
+ "Create a wall type carrying a material and a thickness, by duplicating an existing type and giving it a single structural layer. A wall's material is a property of its type, not of the wall, so this is the only way to get walls that are not the template's default grey: create the type, then pass its name to revit_create_walls as wall_type. A type that already exists by this name is REUSED and comes back with created:false and its current thickness and material — it is not re-cut to match the request. Thickness is feet (Revit internal units). One call is one undo step.",
211
+ HOST_TYPE,
212
+ async ({ name, based_on_type_name, thickness, material_id, material_name }) => {
213
+ try {
214
+ const result = await bridge.call("/walltypes/create", {
215
+ name,
216
+ basedOnTypeName: based_on_type_name,
217
+ thickness,
218
+ materialId: material_id,
219
+ materialName: material_name,
220
+ });
221
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
222
+ } catch (error) {
223
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
224
+ }
225
+ },
226
+ );
227
+
228
+ server.tool(
229
+ "revit_create_floor_type",
230
+ "Create a floor type carrying a material and a thickness, by duplicating an existing type and giving it a single structural layer. Like walls, a floor's material lives on its type — this is how paving stops being grey: create the type, then pass its name to revit_create_floor as type_name. A type that already exists by this name is REUSED and comes back with created:false and its current thickness and material. Thickness is feet (Revit internal units). One call is one undo step.",
231
+ HOST_TYPE,
232
+ async ({ name, based_on_type_name, thickness, material_id, material_name }) => {
233
+ try {
234
+ const result = await bridge.call("/floortypes/create", {
235
+ name,
236
+ basedOnTypeName: based_on_type_name,
237
+ thickness,
238
+ materialId: material_id,
239
+ materialName: material_name,
240
+ });
241
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
242
+ } catch (error) {
243
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
244
+ }
245
+ },
246
+ );
247
+ }