@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,285 @@
1
+ // Site, hardscape and family placement: the tools that put modelled elements in
2
+ // the document rather than drawing-side ones.
3
+ //
4
+ // Like every other write, the Revit side wraps a whole call in one transaction
5
+ // group, so a batch of 200 points is exactly one Ctrl+Z for the user.
6
+ //
7
+ // All lengths and coordinates are Revit internal units: decimal feet.
8
+
9
+ import { z } from "zod";
10
+
11
+ const point3 = z.object({
12
+ x: z.number(),
13
+ y: z.number(),
14
+ z: z.number().optional().describe("Elevation in feet; defaults to 0"),
15
+ });
16
+
17
+ // z is the surface elevation at that point for a toposolid, so it is not optional there.
18
+ const surveyPoint = z.object({
19
+ x: z.number(),
20
+ y: z.number(),
21
+ z: z.number().describe("Surface elevation at this point, in feet"),
22
+ });
23
+
24
+ const point2 = z.object({ x: z.number(), y: z.number() });
25
+
26
+ export function registerModelTools(server, bridge) {
27
+ server.tool(
28
+ "revit_create_toposolid",
29
+ "Create the site surface from a cloud of survey points. Uses a Revit 2024+ Toposolid when the document has a toposolid type, and falls back to the legacy TopographySurface when it has none — the response says which in 'type'. Points are x/y/z in feet (Revit internal units) and z is the surface elevation at that point, so it matters here. One call is one undo step.",
30
+ {
31
+ points: z
32
+ .array(surveyPoint)
33
+ .min(3)
34
+ .describe("Survey points defining the top face, at least 3, in feet"),
35
+ type_name: z
36
+ .string()
37
+ .min(1)
38
+ .optional()
39
+ .describe("Toposolid type name. Omit it to use the first one in the document."),
40
+ level: z
41
+ .string()
42
+ .min(1)
43
+ .optional()
44
+ .describe("Level the toposolid is hosted on. Omit it to use the lowest level in the document."),
45
+ },
46
+ async ({ points, type_name, level }) => {
47
+ try {
48
+ const result = await bridge.call("/toposolid/create", {
49
+ points,
50
+ typeName: type_name,
51
+ level,
52
+ });
53
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
54
+ } catch (error) {
55
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
56
+ }
57
+ },
58
+ );
59
+
60
+ server.tool(
61
+ "revit_flatten_toposolid",
62
+ "Flatten a region of the site surface to one elevation, so paving can sit on graded terrain instead of fighting it — this is the fix for Revit's 'Highlighted toposolid and floor overlap' warning. 'points' is a ring in plan and 'elevation' is what to level it to, both in feet (Revit internal units). The ring is added to the surface at that elevation, creased so the flat region ends at its boundary, and every existing surface point inside it is moved to match. The response reports 'residual' — the largest distance any point in the region is still off the target — so check that rather than assuming it worked. Only a Revit 2024+ Toposolid can be flattened; a legacy TopographySurface cannot. One call is one undo step.",
63
+ {
64
+ points: z
65
+ .array(point2)
66
+ .min(3)
67
+ .describe("Region boundary in plan, in feet. Closed automatically; at least 3 points."),
68
+ elevation: z.number().describe("Elevation to flatten the region to, in feet"),
69
+ toposolid_id: z
70
+ .number()
71
+ .int()
72
+ .optional()
73
+ .describe("Id of the toposolid to flatten. Only needed when the document has more than one."),
74
+ },
75
+ async ({ points, elevation, toposolid_id }) => {
76
+ try {
77
+ const result = await bridge.call("/toposolid/flatten", {
78
+ points,
79
+ elevation,
80
+ toposolidId: toposolid_id,
81
+ });
82
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
83
+ } catch (error) {
84
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
85
+ }
86
+ },
87
+ );
88
+
89
+ server.tool(
90
+ "revit_create_floor",
91
+ "Create a floor from a closed boundary: paving, a pool deck, a terrace, or an actual building floor. The boundary is a ring of x/y points in feet (Revit internal units) taken at the level's elevation, and it is closed automatically when the last point is not the first. 'offset' lifts the floor off its level (Revit's 'Height Offset From Level'), which is how paving clears a graded toposolid instead of interpenetrating it — the other half of that fix is revit_flatten_toposolid. One call is one undo step.",
92
+ {
93
+ level: z.string().min(1).describe("Level name the floor is hosted on"),
94
+ boundary: z
95
+ .array(point2)
96
+ .min(3)
97
+ .describe("Boundary ring in plan, in feet. Closed automatically; at least 3 points."),
98
+ type_name: z
99
+ .string()
100
+ .min(1)
101
+ .optional()
102
+ .describe("Floor type name, e.g. 'Generic - 300mm'. Omit it to use the document default."),
103
+ structural: z
104
+ .boolean()
105
+ .optional()
106
+ .describe("True for a structural floor, false (the default) for architectural"),
107
+ offset: z
108
+ .number()
109
+ .optional()
110
+ .describe(
111
+ "Height offset from the level in feet, positive up. Defaults to 0. Written to the floor's 'Height Offset From Level' parameter and read back into the response.",
112
+ ),
113
+ },
114
+ async ({ level, boundary, type_name, structural, offset }) => {
115
+ try {
116
+ const result = await bridge.call("/floors/create", {
117
+ level,
118
+ boundary,
119
+ typeName: type_name,
120
+ structural,
121
+ offset,
122
+ });
123
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
124
+ } catch (error) {
125
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
126
+ }
127
+ },
128
+ );
129
+
130
+ server.tool(
131
+ "revit_load_families",
132
+ "Load .rfa family files into the project — real trees, benches, bollards, light fittings, doors and windows instead of DirectShape primitives. Autodesk's library is an optional download that lives OUTSIDE the project, under C:\\ProgramData\\Autodesk\\RVT <year>\\Libraries\\<language>\\ (Planting\\, Site\\Accessories\\, Lighting\\Architectural\\External\\, Furniture\\, Doors\\, Windows\\), so nothing in it exists to revit_list_family_symbols or revit_place_families until it is loaded here first. A library one Revit release behind loads fine — Revit upgrades the family on the way in, and anything it warned about comes back in 'warnings'. Every file comes back with the family name and its type ids, so you can place immediately without a second lookup. A family already in the project is reloaded rather than refused, keeping the parameter values the project has set. Read 'loaded' and 'alreadyLoaded' together: loaded false with alreadyLoaded true is not a failure, it means Revit found the project's copy identical to the file and did nothing — the familyName and symbols on that row are still the ones you want. Nothing aborts the batch: a missing file is FILE_NOT_FOUND on its own row and the rest still load. One call is one undo step.",
133
+ {
134
+ paths: z
135
+ .array(z.string().min(1))
136
+ .min(1)
137
+ .optional()
138
+ .describe("Full paths of .rfa files to load. Pass the whole batch in one call."),
139
+ path: z.string().min(1).optional().describe("A single .rfa path, as a shorthand for paths"),
140
+ },
141
+ async ({ paths, path }) => {
142
+ try {
143
+ if (!paths && !path) {
144
+ return {
145
+ content: [
146
+ { type: "text", text: "Error: pass 'paths' (an array of .rfa files) or 'path' (one)." },
147
+ ],
148
+ };
149
+ }
150
+ const result = await bridge.call("/families/load", paths ? { paths } : { path });
151
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
152
+ } catch (error) {
153
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
154
+ }
155
+ },
156
+ );
157
+
158
+ server.tool(
159
+ "revit_list_family_symbols",
160
+ "List the family types loaded in the document: id, family name, type name and category. This is how you discover what revit_place_families can actually place. Filter by 'category', by 'family_name', or both — 'family_name' matches the family, so right after revit_load_families you can ask for exactly the file you just loaded. An empty list means no family content is loaded: load some with revit_load_families, and only fall back to revit_create_directshape or revit_place_planting if the library is genuinely not installed.",
161
+ {
162
+ category: z
163
+ .string()
164
+ .min(1)
165
+ .optional()
166
+ .describe("Category name to filter by, e.g. 'Planting' or 'OST_LightingFixtures'"),
167
+ family_name: z
168
+ .string()
169
+ .min(1)
170
+ .optional()
171
+ .describe(
172
+ "Family name to filter by, e.g. 'M_RPC Tree - Deciduous'. This is the family, not the type — it returns every type in that family.",
173
+ ),
174
+ },
175
+ async ({ category, family_name }) => {
176
+ try {
177
+ const result = await bridge.call("/families/symbols", {
178
+ category,
179
+ familyName: family_name,
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_place_families",
190
+ "Place one family instance per point — trees, shrubs, light fittings, furniture. Get symbol_id from revit_list_family_symbols (and put the family there first with revit_load_families). The symbol is activated for you if it has never been used. Points are x/y/z in feet (Revit internal units) and z is an ABSOLUTE model elevation, the same as everywhere else in this MCP — the bridge converts it to the level offset Revit's placement API actually wants, so a tree asked for at z=0 stands at z=0 whatever level it is on. 'level' is optional and defaults to the lowest level, which is where site content belongs; 'z' is an extra offset added to every point, for lifting a whole batch onto a terrace. Each placed point reports 'placedZ' read back off the instance, so you can check where things actually landed rather than trusting it. rotation is radians about the vertical axis. Pass every point in one call: the whole batch is one undo step, and a point Revit refuses comes back under 'failed' while the rest are still placed.",
191
+ {
192
+ symbol_id: z
193
+ .number()
194
+ .int()
195
+ .describe("Family type id from revit_list_family_symbols"),
196
+ level: z
197
+ .string()
198
+ .min(1)
199
+ .optional()
200
+ .describe("Level name the instances are associated with. Omit it for the lowest level."),
201
+ points: z
202
+ .array(point3)
203
+ .min(1)
204
+ .describe(
205
+ "Insertion points in feet, one instance per point, z an absolute model elevation. All in this one call.",
206
+ ),
207
+ z: z
208
+ .number()
209
+ .optional()
210
+ .describe("Extra elevation in feet added to every point's own z. Defaults to 0."),
211
+ rotation: z
212
+ .number()
213
+ .optional()
214
+ .describe(
215
+ "Rotation in radians about the vertical axis through each point, counter-clockwise in plan. Radians is Revit's internal angle unit, as feet is its internal length unit.",
216
+ ),
217
+ },
218
+ async ({ symbol_id, level, points, z: zOffset, rotation }) => {
219
+ try {
220
+ const result = await bridge.call("/families/place", {
221
+ symbolId: symbol_id,
222
+ level,
223
+ points,
224
+ z: zOffset,
225
+ rotation,
226
+ });
227
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
228
+ } catch (error) {
229
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
230
+ }
231
+ },
232
+ );
233
+
234
+ server.tool(
235
+ "revit_place_openings",
236
+ "Place doors and windows IN walls — the tool for family instances that have to cut their host, which is why it is not revit_place_families with a flag. A door or window placed unhosted stands in front of an uncut wall: nearly right in plan, wrong in every 3D view, and the most common way a model of a house stays a sealed box. Get symbol_id from revit_list_family_symbols after revit_load_families; Autodesk's library keeps them under Doors\\ and Windows\\ (and Doors\\Residential\\ for the exterior ones). The symbol is activated for you if it has never been used. host_wall_id is OPTIONAL and leaving it out is the normal case: for each point the bridge projects onto every wall's location line and hosts in the nearest within 3 feet, then reports the wall each instance actually landed in — read back off the instance's host, not echoed, so a symbol that is not wall-hosted shows up as a null hostWallId rather than being assumed to have worked. A point with no wall near it comes back under 'failed' with NO_HOST_WALL and the rest of the batch still lands. Points are x/y/z in feet (Revit internal units) along the wall; z is the absolute model elevation of the insertion point. sill_height is feet above the level: omit it and a Windows-category symbol gets 3 feet (a window on the floor is not a window) while anything else keeps Revit's own. The row reports sillHeight read back and sillHeightOn — 'instance' normally, 'type' when the family keeps its sill on the type, and writing the type's moves every other instance of that type. Pass every opening in one call: the whole batch is one undo step.",
237
+ {
238
+ symbol_id: z
239
+ .number()
240
+ .int()
241
+ .describe("Door or window family type id from revit_list_family_symbols"),
242
+ points: z
243
+ .array(point3)
244
+ .min(1)
245
+ .describe(
246
+ "Insertion points in feet, one opening per point, z an absolute model elevation. All in this one call.",
247
+ ),
248
+ host_wall_id: z
249
+ .number()
250
+ .int()
251
+ .optional()
252
+ .describe(
253
+ "Wall to host every point in. Omit it to let the bridge host each point in the nearest wall within 3 feet and report which one it picked.",
254
+ ),
255
+ level: z
256
+ .string()
257
+ .min(1)
258
+ .optional()
259
+ .describe("Level name the openings are associated with. Omit it for the lowest level."),
260
+ sill_height: z
261
+ .number()
262
+ .optional()
263
+ .describe(
264
+ "Height of the sill above the level, in feet. Omit it for 3 feet on a window and Revit's own on anything else.",
265
+ ),
266
+ },
267
+ async ({ symbol_id, points, host_wall_id, level, sill_height }) => {
268
+ try {
269
+ // The tool arguments stay snake_case like every other tool's; the bridge
270
+ // reads symbolId / hostWallId / sillHeight — map them here rather than on
271
+ // the C# side.
272
+ const result = await bridge.call("/openings/place", {
273
+ symbolId: symbol_id,
274
+ points,
275
+ hostWallId: host_wall_id,
276
+ level,
277
+ sillHeight: sill_height,
278
+ });
279
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
280
+ } catch (error) {
281
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
282
+ }
283
+ },
284
+ );
285
+ }
@@ -0,0 +1,140 @@
1
+ // Project parameters: making one, and writing a different value per element.
2
+ //
3
+ // A project parameter in Revit is a shared parameter definition plus a binding
4
+ // to categories. The definition lives in a text file outside the model, which
5
+ // is a user-wide Revit setting — the bridge borrows it for the length of the
6
+ // call and puts the original back, so creating a parameter never leaves the
7
+ // user's Revit pointing somewhere new.
8
+ //
9
+ // revit_set_parameters (in write.js) is the other half of this and a different
10
+ // job: it writes ONE name and ONE value across a list of ids, all or nothing.
11
+ // The tool here writes a DIFFERENT value per sheet, which is what stamping a
12
+ // phase across 56 sheets is.
13
+
14
+ import { z } from "zod";
15
+
16
+ export function registerParameterTools(server, bridge) {
17
+ server.tool(
18
+ "revit_create_project_parameter",
19
+ "Create a project parameter and bind it to one or more categories — the way to add a field Revit does not have, such as a Phase on sheets that the Project Browser can group by. Defaults to a Text instance parameter under Identity Data. Pass 'category' for one category or 'categories' for several; both name categories as Revit shows them ('Sheets') or as BuiltInCategory names ('OST_Sheets'). A parameter of that name already bound in this document is NOT an error: nothing is created, 'created' is false and 'alreadyExisted' is true, and the categories reported are the ones it is really bound to — so re-running the same call is safe. One call is one undo step. Write the values afterwards with revit_set_sheet_parameters (per sheet) or revit_set_parameters (one value across many elements).",
20
+ {
21
+ name: z.string().min(1).describe("Parameter name as it will appear in Revit, e.g. 'Phase'"),
22
+ category: z
23
+ .string()
24
+ .min(1)
25
+ .optional()
26
+ .describe("Single category to bind to, e.g. 'Sheets' or 'OST_Sheets'. Use this or categories, not both."),
27
+ categories: z
28
+ .array(z.string().min(1))
29
+ .min(1)
30
+ .optional()
31
+ .describe("Categories to bind to. Use this or category, not both."),
32
+ type: z
33
+ .enum([
34
+ "Text",
35
+ "MultilineText",
36
+ "Url",
37
+ "Integer",
38
+ "Number",
39
+ "YesNo",
40
+ "Length",
41
+ "Area",
42
+ "Volume",
43
+ "Angle",
44
+ ])
45
+ .optional()
46
+ .describe("Parameter data type. Defaults to Text. Length, Area, Volume and Angle are in Revit internal units (feet, square feet, cubic feet, radians)."),
47
+ group: z
48
+ .enum([
49
+ "IdentityData",
50
+ "Text",
51
+ "Data",
52
+ "General",
53
+ "Graphics",
54
+ "Constraints",
55
+ "Geometry",
56
+ "Phasing",
57
+ "Title",
58
+ ])
59
+ .optional()
60
+ .describe("Group the parameter appears under in the Properties palette. Defaults to IdentityData."),
61
+ instance: z
62
+ .boolean()
63
+ .optional()
64
+ .describe("True (the default) binds it per element; false binds it to the type, so every element of that type shares one value."),
65
+ },
66
+ async ({ name, category, categories, type, group, instance }) => {
67
+ // Checked here rather than in the schema: a raw shape cannot express
68
+ // "one of these two", and a call with neither must not reach Revit.
69
+ if (category === undefined && categories === undefined) {
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: "Error: pass either category (one) or categories (several).",
75
+ },
76
+ ],
77
+ };
78
+ }
79
+ if (category !== undefined && categories !== undefined) {
80
+ return {
81
+ content: [
82
+ { type: "text", text: "Error: pass either category or categories, not both." },
83
+ ],
84
+ };
85
+ }
86
+
87
+ try {
88
+ const result = await bridge.call("/parameters/create-project", {
89
+ name,
90
+ category,
91
+ categories,
92
+ type,
93
+ group,
94
+ instance,
95
+ });
96
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
97
+ } catch (error) {
98
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
99
+ }
100
+ },
101
+ );
102
+
103
+ server.tool(
104
+ "revit_set_sheet_parameters",
105
+ "Write a parameter value per sheet — a different value on each, which is what stamping a phase or a discipline across a set of sheets is. Pass every sheet in one call: the whole batch is one undo step, and a sheet whose parameter is missing or read-only comes back under 'failed' with its code while the rest are still written. Instance parameters only: writing to the type would change every other sheet using it. Create the parameter first with revit_create_project_parameter bound to Sheets — a name no sheet carries comes back as PARAMETER_NOT_FOUND. Use revit_set_parameters instead when one value goes on many elements.",
106
+ {
107
+ values: z
108
+ .array(
109
+ z.object({
110
+ sheet_id: z.number().int().describe("Sheet id from revit_list_sheets"),
111
+ name: z
112
+ .string()
113
+ .min(1)
114
+ .describe("Parameter name as it appears in Revit, e.g. 'Phase'"),
115
+ value: z
116
+ .union([z.string(), z.number(), z.boolean()])
117
+ .describe("Value for this sheet. Numbers are in Revit internal units; booleans map to Yes/No parameters."),
118
+ }),
119
+ )
120
+ .min(1)
121
+ .describe("Sheet/name/value triples, all in this one call"),
122
+ },
123
+ async ({ values }) => {
124
+ try {
125
+ // The tool arguments stay snake_case like every other tool's; the bridge
126
+ // reads sheetId — map it here rather than on the C# side.
127
+ const result = await bridge.call("/sheets/set-parameter", {
128
+ values: values.map((entry) => ({
129
+ sheetId: entry.sheet_id,
130
+ name: entry.name,
131
+ value: entry.value,
132
+ })),
133
+ });
134
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
135
+ } catch (error) {
136
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
137
+ }
138
+ },
139
+ );
140
+ }
@@ -0,0 +1,200 @@
1
+ // Quality assurance: the tools that measure what is in the model rather than
2
+ // add to it, plus the one edit safe enough to sit beside them.
3
+ //
4
+ // Every other read here is deliberately compact — id, name, category, type —
5
+ // and compact cannot answer "is this tree standing in the pool", "is that chair
6
+ // buried under the slab", "what grades is the paving sitting on". That needs
7
+ // measured geometry, so this is the one place that reports it at length, and it
8
+ // still bounds what comes back.
9
+ //
10
+ // All coordinates are absolute Revit model coordinates in decimal feet — the
11
+ // document's internal origin, the same frame locations and bounding boxes are
12
+ // reported in, never relative to a level or a view. The responses say so in
13
+ // `coordinateSystem` rather than leaving it to be assumed.
14
+ //
15
+ // Every catch here answers with `isError: true`. A refusal these tools exist to
16
+ // make — a pinned element, CanBeExcavatedBy saying no, a rolled-back
17
+ // transaction — arrives as a bridge exception, and without the flag an MCP
18
+ // client reads it as a successful call whose text happens to start with
19
+ // "Error:". A write that did not happen must not look like one that did.
20
+
21
+ import { z } from "zod";
22
+ import { clampLimit, DEFAULT_QUERY_LIMIT, MAX_QUERY_LIMIT } from "./read.js";
23
+
24
+ // The bridge refuses more than this per inspect call rather than clamping: a
25
+ // silently shortened inspection reads as a complete one.
26
+ export const MAX_INSPECT_IDS = 500;
27
+
28
+ export function registerQualityTools(server, bridge) {
29
+ server.tool(
30
+ "revit_inspect_elements",
31
+ "Measure elements: everything revit_get_elements leaves out. Per element — id, name, uniqueId, Revit class, category, typeId, typeName, level, hostId, pinned, groupId, its location (a point, or a curve's endpoints), its model bounding box and its material ids. A floor also reports its level, height offset and the closed loops of its top face; a toposolid (or legacy topography) reports its shape vertices with absolute positions, which is the only way to see the grades paving and planting sit on; a family instance reports its symbol, facing and hand vectors and the Z it ACTUALLY sits at. All coordinates are absolute model feet. Ids with no element come back in missingIds — nothing is dropped silently. Max 500 ids per call. With include_parameters, every parameter also reports its own id and BuiltInParameter name, and duplicateParameters lists the names Revit uses TWICE on one element (a family instance has two called 'Level', one read-only) — read that before writing anything by name with revit_set_parameters.",
32
+ {
33
+ ids: z
34
+ .array(z.number().int())
35
+ .min(1)
36
+ .max(MAX_INSPECT_IDS)
37
+ .describe(
38
+ `Element ids to inspect (max ${MAX_INSPECT_IDS} — over that is rejected, not truncated)`,
39
+ ),
40
+ include_parameters: z
41
+ .boolean()
42
+ .default(false)
43
+ .describe(
44
+ "Also return every instance parameter, each with its parameter id and built-in name, plus duplicateParameters for names Revit uses more than once. Off by default: it is hundreds of lines per element when three of them were the question.",
45
+ ),
46
+ include_geometry: z
47
+ .boolean()
48
+ .optional()
49
+ .describe(
50
+ "Force vertex/segment lists on or off. Omit it and they come back whenever there are 500 or fewer, and collapse to a count plus a min/max Z above that. true asks for them anyway, capped at 500 with truncated: true.",
51
+ ),
52
+ },
53
+ async ({ ids, include_parameters, include_geometry }) => {
54
+ try {
55
+ // The tool arguments stay snake_case like every other tool's, but the
56
+ // bridge reads camelCase — map it here rather than on the C# side.
57
+ const result = await bridge.call("/elements/inspect", {
58
+ ids,
59
+ includeParameters: include_parameters,
60
+ includeGeometry: include_geometry,
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_move_elements",
74
+ "Move elements by a vector in feet — lift furniture out of a slab, shift a tree off a path — without rebuilding them. DRY RUN BY DEFAULT: dry_run is true unless you pass false, and a dry run opens no transaction and changes nothing. Either way every element comes back with before and after location AND bounding box, so you never have to guess where something ended up; on a real move the after is read back off the element, on a dry run it is arithmetic (afterSource says which). All-or-nothing: the whole batch moves in one transaction and one undo step, and the request is refused before anything is touched if any id is missing, pinned (the bridge will NOT unpin for you) or in a group. Nothing is ever deleted. A real move is MEASURED before it commits: Revit accepts a move it then does not apply — a family instance whose elevation comes from its level is the everyday case — so if any element did not take the displacement asked for, the whole request is rolled back with MOVE_NOT_APPLIED naming requested against actual, never reported as moved. Ids with nothing measurable come back in unverified.",
75
+ {
76
+ ids: z.array(z.number().int()).min(1).describe("Element ids to move"),
77
+ translation: z
78
+ .object({
79
+ x: z.number().describe("East/west offset in feet"),
80
+ y: z.number().describe("North/south offset in feet"),
81
+ z: z.number().describe("Vertical offset in feet — positive is up"),
82
+ })
83
+ .describe("How far to move, in feet (Revit internal units). Finite numbers only."),
84
+ dry_run: z
85
+ .boolean()
86
+ .default(true)
87
+ .describe(
88
+ "true (the default) measures and reports what the move would do without changing the model. Pass false to actually move.",
89
+ ),
90
+ },
91
+ async ({ ids, translation, dry_run }) => {
92
+ try {
93
+ const result = await bridge.call("/elements/move", {
94
+ ids,
95
+ translation,
96
+ dryRun: dry_run,
97
+ });
98
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
99
+ } catch (error) {
100
+ return {
101
+ content: [{ type: "text", text: `Error: ${error.message}` }],
102
+ isError: true,
103
+ };
104
+ }
105
+ },
106
+ );
107
+
108
+ server.tool(
109
+ "revit_excavate_toposolid",
110
+ "Cut a toposolid with the elements that should be sunk into it — a pool, a basement, a sunken path — using Revit's native Toposolid.ExcavateBy. This is the NON-DESTRUCTIVE way to make a floor or a pool clear the terrain: revit_flatten_toposolid rewrites the grades under a region and that ground never comes back, while an excavation is an association, so the surface keeps every vertex and the hole follows the element that made it. DRY RUN BY DEFAULT: dry_run is true unless you pass false, and a dry run opens no transaction. Every id is checked with Revit's own CanBeExcavatedBy before anything is opened, and the whole batch goes in one transaction and one undo step. The response reports the toposolid's volume before and after and the volume removed, plus the volume Revit attributes to each element — an element that was excavated but removed nothing does not actually overlap the surface. Nothing is ever deleted.",
111
+ {
112
+ toposolid_id: z
113
+ .number()
114
+ .int()
115
+ .optional()
116
+ .describe(
117
+ "The toposolid to cut. Optional only when the document has exactly one — with several, it is required, and the error lists them.",
118
+ ),
119
+ ids: z
120
+ .array(z.number().int())
121
+ .min(1)
122
+ .describe(
123
+ "Ids of the elements that cut the terrain (the pool, the floor, the mass) — not the toposolid itself",
124
+ ),
125
+ dry_run: z
126
+ .boolean()
127
+ .default(true)
128
+ .describe(
129
+ "true (the default) runs the CanBeExcavatedBy preflight and reports the current volume without changing the model. Pass false to actually excavate.",
130
+ ),
131
+ },
132
+ async ({ toposolid_id, ids, dry_run }) => {
133
+ try {
134
+ const result = await bridge.call("/toposolid/excavate", {
135
+ toposolidId: toposolid_id,
136
+ ids,
137
+ dryRun: dry_run,
138
+ });
139
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
140
+ } catch (error) {
141
+ return {
142
+ content: [{ type: "text", text: `Error: ${error.message}` }],
143
+ isError: true,
144
+ };
145
+ }
146
+ },
147
+ );
148
+
149
+ server.tool(
150
+ "revit_get_warnings",
151
+ "Read the warnings the model is carrying right now — Revit's own Review Warnings list. Each one carries the GUID of its failure definition (the only stable identity a warning kind has; the message text is localised), its severity, its message, the element ids it is about and any additional ids. Read-only. This is NOT revit_diagnostics: that one is what the bridge suppressed during your writes, this one is the standing state of the model. Answers with the full total, so paging with offset tells you what you have not seen.",
152
+ {
153
+ limit: z
154
+ .number()
155
+ .int()
156
+ .positive()
157
+ .default(DEFAULT_QUERY_LIMIT)
158
+ .describe(
159
+ `Max warnings to return (default ${DEFAULT_QUERY_LIMIT}, hard cap ${MAX_QUERY_LIMIT} — higher values are clamped, not rejected)`,
160
+ ),
161
+ offset: z
162
+ .number()
163
+ .int()
164
+ .min(0)
165
+ .default(0)
166
+ .describe("Warnings to skip, for paging through a noisy model"),
167
+ },
168
+ async ({ limit, offset }) => {
169
+ try {
170
+ const result = await bridge.call("/document/warnings", {
171
+ limit: clampLimit(limit),
172
+ offset,
173
+ });
174
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
175
+ } catch (error) {
176
+ return {
177
+ content: [{ type: "text", text: `Error: ${error.message}` }],
178
+ isError: true,
179
+ };
180
+ }
181
+ },
182
+ );
183
+
184
+ server.tool(
185
+ "revit_list_view_templates",
186
+ "List the view templates in the model: id, name, view type, and the parameters each template CONTROLS with their labels. Read-only. Check this before setting a scale, a display style or a category override on a view — a parameter the template controls is one the view cannot hold its own value for, which is why a setting you wrote reads back as something else.",
187
+ {},
188
+ async () => {
189
+ try {
190
+ const result = await bridge.call("/views/templates");
191
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
192
+ } catch (error) {
193
+ return {
194
+ content: [{ type: "text", text: `Error: ${error.message}` }],
195
+ isError: true,
196
+ };
197
+ }
198
+ },
199
+ );
200
+ }