@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,142 @@
1
+ // Read-only tools. Intent-level, not a mirror of the Revit API: every response
2
+ // is compact JSON — element ids plus only the parameters that were asked for.
3
+ // Dumping full parameter sets would bury the model in noise for no gain.
4
+
5
+ import { z } from "zod";
6
+
7
+ export const DEFAULT_QUERY_LIMIT = 100;
8
+ export const MAX_QUERY_LIMIT = 500;
9
+
10
+ // A model that asks for 10000 rows gets 500, not an error — the response always
11
+ // carries `total`, so it can see what it did not get and page with offset.
12
+ export function clampLimit(limit) {
13
+ if (!Number.isFinite(limit)) return DEFAULT_QUERY_LIMIT;
14
+ return Math.min(Math.max(Math.trunc(limit), 1), MAX_QUERY_LIMIT);
15
+ }
16
+
17
+ export function registerReadTools(server, bridge) {
18
+ server.tool(
19
+ "revit_status",
20
+ "Check whether Revit is reachable: version, active document name/path, and whether the model is workshared. Run this first when any other Revit tool fails.",
21
+ {},
22
+ async () => {
23
+ try {
24
+ const result = await bridge.call("/status");
25
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
26
+ } catch (error) {
27
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
28
+ }
29
+ },
30
+ );
31
+
32
+ server.tool(
33
+ "revit_list_levels",
34
+ "List the levels in the active document: id, name and elevation (Revit internal units, decimal feet).",
35
+ {},
36
+ async () => {
37
+ try {
38
+ const result = await bridge.call("/levels");
39
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
40
+ } catch (error) {
41
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
42
+ }
43
+ },
44
+ );
45
+
46
+ server.tool(
47
+ "revit_list_categories",
48
+ "List the categories present in the active document with an element count each. Use this to find the exact category name to pass to revit_query_elements.",
49
+ {},
50
+ async () => {
51
+ try {
52
+ const result = await bridge.call("/categories");
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_query_elements",
62
+ "Find elements by category and/or level and/or type name. Returns compact rows (id, name, category, level, type) plus the total match count, so you always know how much you did not see. Page with offset.",
63
+ {
64
+ category: z
65
+ .string()
66
+ .optional()
67
+ .describe("Category name as shown by revit_list_categories (e.g. 'Walls')"),
68
+ level: z.string().optional().describe("Level name (e.g. 'Level 1')"),
69
+ type_name: z
70
+ .string()
71
+ .optional()
72
+ .describe("Element type name, matched case-insensitively as a substring"),
73
+ limit: z
74
+ .number()
75
+ .int()
76
+ .positive()
77
+ .default(DEFAULT_QUERY_LIMIT)
78
+ .describe(
79
+ `Max rows to return (default ${DEFAULT_QUERY_LIMIT}, hard cap ${MAX_QUERY_LIMIT} — higher values are clamped, not rejected)`,
80
+ ),
81
+ offset: z
82
+ .number()
83
+ .int()
84
+ .min(0)
85
+ .default(0)
86
+ .describe("Rows to skip, for paging through a large result"),
87
+ },
88
+ async ({ category, level, type_name, limit, offset }) => {
89
+ try {
90
+ // The tool argument stays snake_case like every other tool's, but the
91
+ // bridge reads `typeName` — map it here rather than on the C# side.
92
+ const result = await bridge.call("/query", {
93
+ category,
94
+ level,
95
+ typeName: type_name,
96
+ limit: clampLimit(limit),
97
+ offset,
98
+ });
99
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
100
+ } catch (error) {
101
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
102
+ }
103
+ },
104
+ );
105
+
106
+ server.tool(
107
+ "revit_get_elements",
108
+ "Read specific elements by id. Pass params to get exactly those parameters and nothing else; without params you get identity only (id, name, category, type).",
109
+ {
110
+ ids: z
111
+ .array(z.number().int())
112
+ .min(1)
113
+ .describe("Element ids (from revit_query_elements or revit_get_selection)"),
114
+ params: z
115
+ .array(z.string())
116
+ .optional()
117
+ .describe("Parameter names to read, e.g. ['Comments', 'Mark', 'Unconnected Height']"),
118
+ },
119
+ async ({ ids, params }) => {
120
+ try {
121
+ const result = await bridge.call("/elements", { ids, params });
122
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
123
+ } catch (error) {
124
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
125
+ }
126
+ },
127
+ );
128
+
129
+ server.tool(
130
+ "revit_get_selection",
131
+ "Read what the user currently has selected in the Revit UI. Use this when the user says 'this wall', 'the selected elements' or similar.",
132
+ {},
133
+ async () => {
134
+ try {
135
+ const result = await bridge.call("/selection");
136
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
137
+ } catch (error) {
138
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
139
+ }
140
+ },
141
+ );
142
+ }
@@ -0,0 +1,27 @@
1
+ // Hot reload: swap the bridge's routing and endpoint logic without restarting
2
+ // Revit.
3
+ //
4
+ // A Revit restart means closing the model, which is precisely what a long
5
+ // unattended run cannot afford — so the add-in is split in two. The half Revit
6
+ // pins for the session owns the HTTP listener and the main-thread pump; the
7
+ // half that changes (routes and endpoints) is loaded into a collectible load
8
+ // context from a shadow copy, and this tool replaces it live.
9
+ //
10
+ // Maintainer tooling: it only does anything after the logic DLL has actually
11
+ // been rebuilt on disk.
12
+
13
+ export function registerReloadTools(server, bridge) {
14
+ server.tool(
15
+ "revit_reload_bridge",
16
+ "Reload the bridge's endpoint logic from disk without restarting Revit, after rebuilding the add-in. Use this while developing the bridge itself: it swaps the freshly built logic assembly into the running Revit, keeping the open model, the HTTP listener and any queued work alive. The response says whether the previous version was actually unloaded — if unloadedPrevious is false, the new logic IS live but the old one is still in memory, which is worth investigating rather than ignoring. Nothing changes until the DLL on disk changes, so build first.",
17
+ {},
18
+ async () => {
19
+ try {
20
+ const result = await bridge.call("/reload");
21
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
22
+ } catch (error) {
23
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
24
+ }
25
+ },
26
+ );
27
+ }
@@ -0,0 +1,62 @@
1
+ // Sheet collections: Revit's own collapsible groups under Sheets in the Project
2
+ // Browser. A native SheetCollection element, so the grouping is there when the
3
+ // model opens — no browser-organisation parameter for the user to wire up by
4
+ // hand, and no renaming or renumbering of the sheets themselves.
5
+
6
+ import { z } from "zod";
7
+
8
+ export function registerSheetCollectionTools(server, bridge) {
9
+ server.tool(
10
+ "revit_list_sheet_collections",
11
+ "List the sheet collections in the active document — the collapsible groups Revit draws under Sheets in the Project Browser — with each collection's id, name and member sheets (id, number, name). 'unassignedSheets' is every sheet in no collection. Read-only.",
12
+ {},
13
+ async () => {
14
+ try {
15
+ const result = await bridge.call("/sheets/collections");
16
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
17
+ } catch (error) {
18
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
19
+ }
20
+ },
21
+ );
22
+
23
+ server.tool(
24
+ "revit_set_sheet_collections",
25
+ "Put sheets into native sheet collections, so the Project Browser shows them as collapsible groups. Pass every collection in one call: together they are one undo step. A collection whose name already exists is REUSED, not duplicated, and one whose membership already matches is reported 'unchanged' and left alone. dry_run defaults to TRUE — the call reports the plan and writes nothing until you pass dry_run: false. Nothing else about the sheets is touched: numbers and names are left as they are, and collections this call does not mention keep their members. A sheet belongs to one collection, so an id may appear in only one entry, and assembly sheets cannot join a collection at all. Collections are one level deep; the views under a sheet stay where Revit puts them. The reply reports 'action' (created / reused / unchanged) per collection and, once applied, the membership read back off the committed document.",
26
+ {
27
+ collections: z
28
+ .array(
29
+ z.object({
30
+ name: z
31
+ .string()
32
+ .min(1)
33
+ .describe(
34
+ "Collection name as it should read in the browser, e.g. 'L.02'. Matched exactly against existing collections; Revit prohibits the characters {}[]|;<>?`~ in one.",
35
+ ),
36
+ sheet_ids: z
37
+ .array(z.number().int())
38
+ .min(1)
39
+ .describe("Ids of the sheets that belong in it, from revit_list_sheets"),
40
+ }),
41
+ )
42
+ .min(1)
43
+ .describe("The collections to build, all in this one call"),
44
+ dry_run: z
45
+ .boolean()
46
+ .optional()
47
+ .describe("Defaults to true: report the plan without writing. Pass false to apply it."),
48
+ },
49
+ async ({ collections, dry_run }) => {
50
+ try {
51
+ // Tool arguments stay snake_case; the bridge reads sheetIds / dryRun.
52
+ const result = await bridge.call("/sheets/set-collections", {
53
+ collections: collections.map(({ name, sheet_ids }) => ({ name, sheetIds: sheet_ids })),
54
+ dryRun: dry_run,
55
+ });
56
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
57
+ } catch (error) {
58
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
59
+ }
60
+ },
61
+ );
62
+ }
@@ -0,0 +1,76 @@
1
+ // Sheet tools: the two reads a caller needs before creating sheets, and the
2
+ // batch create itself.
3
+ //
4
+ // A sheet cannot exist without a title block family type, so the read that
5
+ // lists them is part of this set rather than of the general reads.
6
+
7
+ import { z } from "zod";
8
+
9
+ export function registerSheetTools(server, bridge) {
10
+ server.tool(
11
+ "revit_list_titleblocks",
12
+ "List the title block family types loaded in the active document: id, family name and type name. Call this before revit_create_sheets — every sheet needs a title block, and this is where its id comes from. An empty list means no title block family is loaded, so sheets cannot be created yet.",
13
+ {},
14
+ async () => {
15
+ try {
16
+ const result = await bridge.call("/titleblocks");
17
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
18
+ } catch (error) {
19
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
20
+ }
21
+ },
22
+ );
23
+
24
+ server.tool(
25
+ "revit_list_sheets",
26
+ "List the sheets in the active document: id, sheet number and name, ordered by sheet number.",
27
+ {},
28
+ async () => {
29
+ try {
30
+ const result = await bridge.call("/sheets");
31
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
32
+ } catch (error) {
33
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
34
+ }
35
+ },
36
+ );
37
+
38
+ server.tool(
39
+ "revit_create_sheets",
40
+ "Create sheets in the active document. Pass every sheet in one call: all of them together are one undo step. A sheet number that already exists is skipped and reported in 'skipped' rather than erroring, so re-running the same call is safe.",
41
+ {
42
+ sheets: z
43
+ .array(
44
+ z.object({
45
+ number: z
46
+ .string()
47
+ .min(1)
48
+ .describe("Sheet number, e.g. 'A101'. Must be unique; an existing one is skipped."),
49
+ name: z.string().min(1).describe("Sheet name, e.g. 'Ground Floor Plan'"),
50
+ }),
51
+ )
52
+ .min(1)
53
+ .describe("Sheets to create, all in this one call"),
54
+ title_block_id: z
55
+ .number()
56
+ .int()
57
+ .optional()
58
+ .describe(
59
+ "Title block family type id from revit_list_titleblocks. Omit it to use the first one loaded.",
60
+ ),
61
+ },
62
+ async ({ sheets, title_block_id }) => {
63
+ try {
64
+ // The tool argument stays snake_case like every other tool's, but the
65
+ // bridge reads `titleBlockId` — map it here rather than on the C# side.
66
+ const result = await bridge.call("/sheets/create", {
67
+ sheets,
68
+ titleBlockId: title_block_id,
69
+ });
70
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
71
+ } catch (error) {
72
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
73
+ }
74
+ },
75
+ );
76
+ }
@@ -0,0 +1,152 @@
1
+ // Title block families, read from the inside.
2
+ //
3
+ // `revit_list_titleblocks` says which title block types are loaded and nothing
4
+ // more. What is actually printed on the sheet — the logo, the consultant
5
+ // placeholders somebody typed into the stock family, the label that grows to
6
+ // hold a long project name — lives in the FAMILY, and Document.EditFamily is
7
+ // the only way to see it from the API.
8
+ //
9
+ // That call hands back an independent copy of the family as its own document.
10
+ // The inspect tool reads it and closes it without saving, inside a finally:
11
+ // nothing in the project, the loaded family or the .rfa on disk changes.
12
+ //
13
+ // `revit_edit_titleblock_family` is the write half, and it takes the ids the
14
+ // inspection reported. It edits the same copy and loads it back into the SAME
15
+ // project with Document.LoadFamily — no SaveAs, no second project file. Reading
16
+ // first is not optional: every id is a family id, and the edit is refused unless
17
+ // expected_family_name matches the family those ids came out of.
18
+
19
+ import { z } from "zod";
20
+
21
+ // A point on the sheet. Annotation in a title block is paper feet, so z is not a
22
+ // dimension it has.
23
+ const point2 = z.object({ x: z.number(), y: z.number() });
24
+
25
+ export function registerTitleblockTools(server, bridge) {
26
+ server.tool(
27
+ "revit_inspect_titleblock_family",
28
+ "Read what is INSIDE a title block family: the only way to find out why a sheet prints a vendor logo, a literal consultant placeholder or a label that overlaps its neighbour. READ-ONLY — the family is opened with Document.EditFamily, which hands back an independent copy, and that copy is closed without saving in a finally; no transaction is opened, so the project, the loaded family and the .rfa on disk are untouched, and nothing here can be undone because nothing is done. Per element it reports id, the Revit API class, category, name, type id/name, the view it is drawn in, its location and its bounding box, plus: text (content, insertion point, width/height, both alignments, text type and that type's text size, and isTextNote — true is literal text somebody typed, false is the other kind of text element); images (size, scale, and the image type's path, source, status and pixel size); imports (whether linked, and the import type's name, which is the file it came from); curves (line style); dimensions (value, isLocked, segments, and the family parameter labelling it — the constraints holding the border together); reference planes (both ends and the normal). The dynamic half is familyParameters: every family parameter with its storage type, formula, instance/type flag, the current type's value, and associatedElementIds — the elements whose own parameters Revit has associated to it. The same association appears on each element row as labelOf. That association is the evidence for which content is parameter-driven and must be preserved and which is a literal string safe to remove; it is reported as the association Revit holds, not as a claim about what a reader sees. Lengths are Revit internal units (decimal feet) and annotation inside a title block is PAPER feet — 5 mm text reads 0.0164. symbol_id is a title block type id from revit_list_titleblocks; anything in another category is refused, and an in-place or non-editable family is refused before anything is opened.",
29
+ {
30
+ symbol_id: z
31
+ .number()
32
+ .int()
33
+ .describe("Id of the loaded title block TYPE (a FamilySymbol), from revit_list_titleblocks"),
34
+ },
35
+ async ({ symbol_id }) => {
36
+ try {
37
+ // Tool arguments stay snake_case; the bridge reads symbolId.
38
+ const result = await bridge.call("/families/titleblock-inspect", { symbolId: symbol_id });
39
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
40
+ } catch (error) {
41
+ return {
42
+ content: [{ type: "text", text: `Error: ${error.message}` }],
43
+ isError: true,
44
+ };
45
+ }
46
+ },
47
+ );
48
+
49
+ server.tool(
50
+ "revit_edit_titleblock_family",
51
+ "Edit the INSIDE of a title block family and load it back into this same project: remove the stock logo and the literal consultant placeholders, retext the captions, resize labels, add notes. The ids are family ids from revit_inspect_titleblock_family, never project ids — inspect first, always. What makes it safe is what it refuses: expected_family_name is required and must match the family those ids came out of, or nothing is even opened; only a TextNote or an ImageInstance can be removed, and a label (a text element bound to a parameter) is refused by name, because deleting one takes that content off every sheet for good; Document.Delete is read back, so anything it would take that you did not name rolls the whole edit back, and so does any label or schedule instance that existed before the edit and not after it. label_sizes does NOT edit the text type in place — a type is shared, and editing it would resize everything on it. An existing type of that size is reused, otherwise the element's own type is duplicated and only the elements named are pointed at the copy; an element Revit will not retype comes back as action 'refused' with the reason, and the removals still stand. Sizes and points are Revit internal units (decimal feet), and annotation inside a title block is PAPER feet: 6 mm text is 0.019685, 3 mm is 0.009843, and 1032 mm across the sheet is 3.385827. dry_run DEFAULTS TO TRUE: it opens the family, resolves every id and every text type, reports exactly what it would do, and closes the copy without saving. Read that, then call again with dry_run false. Applying loads the edited family into the project — every sheet using it redraws — and writes nothing to the .rfa on disk.",
52
+ {
53
+ symbol_id: z
54
+ .number()
55
+ .int()
56
+ .describe("Id of the loaded title block TYPE (a FamilySymbol), from revit_list_titleblocks"),
57
+ expected_family_name: z
58
+ .string()
59
+ .describe(
60
+ "The family name the ids were read from, as revit_inspect_titleblock_family reported it. A mismatch is refused before the family is opened.",
61
+ ),
62
+ remove_ids: z
63
+ .array(z.number().int())
64
+ .optional()
65
+ .describe(
66
+ "Family ids to delete. TextNote and ImageInstance only — the stock logo and the literal placeholders. A label, a line, a dimension or a reference plane is refused.",
67
+ ),
68
+ text_edits: z
69
+ .array(
70
+ z.object({
71
+ id: z.number().int().describe("Family id of a TextNote — literal text, not a label"),
72
+ text: z.string().describe("The text it should read instead"),
73
+ }),
74
+ )
75
+ .optional()
76
+ .describe("Retext literal notes, e.g. translating the stock captions"),
77
+ label_sizes: z
78
+ .array(
79
+ z.object({
80
+ id: z.number().int().describe("Family id of a label or a text note"),
81
+ size: z
82
+ .number()
83
+ .positive()
84
+ .describe("Text size in decimal feet — PAPER feet: 6 mm is 0.019685"),
85
+ }),
86
+ )
87
+ .optional()
88
+ .describe(
89
+ "Retype text elements to a type of this size, reusing one the family has or duplicating theirs",
90
+ ),
91
+ new_notes: z
92
+ .array(
93
+ z.object({
94
+ text: z.string(),
95
+ point: point2.describe("Insertion point on the sheet, in decimal feet"),
96
+ size: z.number().positive().describe("Text size in decimal feet"),
97
+ width: z
98
+ .number()
99
+ .positive()
100
+ .optional()
101
+ .describe(
102
+ "Line-wrapping width in decimal feet. Omit for a single line sized to the text. A width outside what Revit allows for the type is refused with the allowed range.",
103
+ ),
104
+ }),
105
+ )
106
+ .optional()
107
+ .describe("Notes to add in the family's sheet view"),
108
+ view_id: z
109
+ .number()
110
+ .int()
111
+ .optional()
112
+ .describe(
113
+ "View inside the family to draw new_notes in. Defaults to the view the family's existing text is already in; only needed when it draws text in more than one.",
114
+ ),
115
+ dry_run: z
116
+ .boolean()
117
+ .default(true)
118
+ .describe(
119
+ "DEFAULTS TO TRUE. True resolves every id and text type and reports what it would do, changing nothing. Pass false to apply it and load the family back.",
120
+ ),
121
+ },
122
+ async ({
123
+ symbol_id,
124
+ expected_family_name,
125
+ remove_ids,
126
+ text_edits,
127
+ label_sizes,
128
+ new_notes,
129
+ view_id,
130
+ dry_run,
131
+ }) => {
132
+ try {
133
+ const result = await bridge.call("/families/titleblock-edit", {
134
+ symbolId: symbol_id,
135
+ expectedFamilyName: expected_family_name,
136
+ removeIds: remove_ids,
137
+ textEdits: text_edits,
138
+ labelSizes: label_sizes,
139
+ newNotes: new_notes,
140
+ viewId: view_id,
141
+ dryRun: dry_run,
142
+ });
143
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
144
+ } catch (error) {
145
+ return {
146
+ content: [{ type: "text", text: `Error: ${error.message}` }],
147
+ isError: true,
148
+ };
149
+ }
150
+ },
151
+ );
152
+ }