@rui.branco/revit-mcp 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/PRIVACY.md ADDED
@@ -0,0 +1,95 @@
1
+ # Privacy Policy
2
+
3
+ **Effective date:** 21 September 2026
4
+ **Applies to:** the `revit-mcp` MCP server and its bundled Revit bridge add-in
5
+ (the "Software"), distributed as the npm package `@rui.branco/revit-mcp` and as
6
+ the Claude Desktop extension `revit-mcp.mcpb`.
7
+
8
+ ## Summary
9
+
10
+ The Software runs entirely on your own computer. It has no backend, no account,
11
+ no telemetry and no analytics. It sends nothing to the author, and it does not
12
+ transmit your data anywhere over the internet.
13
+
14
+ ## Data collection
15
+
16
+ **The author collects no data whatsoever.** The Software contains no telemetry,
17
+ crash reporting, usage analytics, update check or licence check. No personal
18
+ information, model content, file path or identifier is transmitted to the author
19
+ or to any third party operated by the author.
20
+
21
+ ## What the Software processes, and where
22
+
23
+ The Software reads and edits the Revit model open on your machine. That data is
24
+ processed locally and in memory:
25
+
26
+ - The Node MCP server exchanges JSON with the bridge add-in strictly over
27
+ loopback HTTP at `http://127.0.0.1:48884`. The listener binds the `127.0.0.1`
28
+ literal, not a wildcard, so it is not reachable from your network.
29
+ - The bridge add-in runs inside `Revit.exe` and calls the Revit API directly.
30
+ - Model data leaves your machine only if your MCP client sends it onward. When
31
+ the client is Claude, your prompts and the tool results the model sees are
32
+ handled under **Anthropic's** privacy policy, not this one:
33
+ <https://www.anthropic.com/legal/privacy>
34
+
35
+ Choosing what to ask Claude, and therefore what model content is included in a
36
+ conversation, is yours to control.
37
+
38
+ ## Local storage on your machine
39
+
40
+ The Software writes only these, all locally, and never transmits them:
41
+
42
+ | What | Where | Why |
43
+ | --- | --- | --- |
44
+ | Bridge log | `%LOCALAPPDATA%\RevitMcpBridge\bridge.log` | Diagnosing failures. May contain Revit element ids, view and sheet names, and error text. |
45
+ | Bridge add-in files | `%APPDATA%\Autodesk\Revit\Addins\<version>\` | The installed add-in and its `.addin` manifest. |
46
+ | Diagnostics buffer | In memory, inside Revit | The last 200 dismissed dialogs and resolved warnings. Lost when Revit closes. |
47
+ | Exports you request | The path you specify | Images and PDFs the Software writes only when a tool is called with that path. |
48
+
49
+ Delete the log file at any time; the Software recreates it only when it next
50
+ logs. Uninstalling the bridge (`revit_uninstall_bridge`) removes the add-in
51
+ files.
52
+
53
+ ## Third-party sharing
54
+
55
+ **None.** The author does not share, sell, rent or disclose any data, because the
56
+ author never receives any.
57
+
58
+ The Software makes no outbound internet request of its own. Network activity you
59
+ may observe around it comes from other parties under their own policies: your MCP
60
+ client contacting its provider, and `npm`/`npx` downloading the package from
61
+ <https://registry.npmjs.org> at install time.
62
+
63
+ ## Data retention
64
+
65
+ The author retains nothing, having received nothing. Local files listed above
66
+ stay on your machine until you delete them or uninstall the Software. The
67
+ in-memory diagnostics buffer holds at most 200 entries and is discarded when
68
+ Revit closes.
69
+
70
+ ## Children
71
+
72
+ The Software is a professional tool for Autodesk Revit and is not directed at
73
+ children under 13.
74
+
75
+ ## Security
76
+
77
+ The bridge add-in exposes an **unauthenticated** loopback HTTP listener with full
78
+ Revit API access while Revit is running: any local process on your machine can
79
+ reach it. The add-in is not code-signed. Review the
80
+ [Security](README.md#security) section of the README before installing on a
81
+ shared or untrusted machine.
82
+
83
+ ## Changes to this policy
84
+
85
+ Material changes will be published in this file, with the effective date above
86
+ updated, and will appear in the repository's commit history:
87
+ <https://github.com/rui-branco/revit-mcp/commits/main/PRIVACY.md>
88
+
89
+ ## Contact
90
+
91
+ Questions, or a privacy or security concern:
92
+
93
+ - Issues: <https://github.com/rui-branco/revit-mcp/issues>
94
+ - Private security reports:
95
+ <https://github.com/rui-branco/revit-mcp/security/advisories/new>
package/README.md CHANGED
@@ -10,72 +10,77 @@
10
10
  it, and produce a full drawing set in plain language — levels and elements,
11
11
  sheets and schedules, views and PDF exports.
12
12
 
13
- **Windows · Revit 2025, 2026 or 2027 · Node.js 18+.** The Revit add-in ships
14
- precompiled, so there is nothing to build and no .NET SDK to install. `npm` is
15
- never run at startup the Desktop extension carries its dependencies, and the
16
- Claude Code route fetches the package once via `npx`.
13
+ **Windows · Revit 2025, 2026 or 2027.** The add-in ships precompiled — nothing
14
+ to build, no .NET SDK. The Claude Desktop extension needs no Node.js either; the
15
+ Claude Code route uses `npx`, so it wants Node 18+.
17
16
 
18
17
  ## Installation
19
18
 
20
19
  ### 1. Register the server
21
20
 
22
- #### Claude Desktop — extension
21
+ Two commands, in **Claude Code or Claude Desktop**same plugin, either app:
23
22
 
24
- 1. **[⬇ Download `revit-mcp.mcpb`](https://github.com/rui-branco/revit-mcp/releases/latest/download/revit-mcp.mcpb)**
25
- 2. In Claude Desktop, open **Settings → Extensions**
26
- 3. **Drag the `.mcpb` file onto that window**, then press **Install**
27
-
28
- No JSON to edit and nothing to download at runtime: the extension carries the
29
- server, its dependencies and the Revit add-in. Bridge URL and request timeout
30
- are exposed as settings under **Settings → Extensions → Revit**.
23
+ ```
24
+ /plugin marketplace add rui-branco/revit-mcp
25
+ /plugin install revit@revit-tools
26
+ ```
31
27
 
32
- > Double-clicking the file only works if Claude Desktop has registered the
33
- > `.mcpb` file type; on many installs it has not, so use the drag-and-drop step
34
- > above.
28
+ That is the whole step. The plugin updates itself from this repo, so there is
29
+ nothing to re-download when a new version ships; `/plugin update` pulls it
30
+ early if you do not want to wait.
35
31
 
36
32
  <details>
37
- <summary>Or configure it by hand</summary>
33
+ <summary><b>Other ways to install</b></summary>
34
+
35
+ These exist for completeness. The plugin above is the supported path, and the
36
+ only one that updates on its own.
37
+
38
+ **Claude Code, without the plugin**
39
+
40
+ ```bash
41
+ claude mcp add revit --scope user -- npx -y @rui.branco/revit-mcp
42
+ ```
43
+
44
+ `--scope user` enables it in every project; `--scope project` writes it to the
45
+ repo's `.mcp.json` instead, to share with a team.
46
+
47
+ **Claude Desktop, as a Desktop Extension (.mcpb)**
38
48
 
39
- Open **Settings → Developer → Edit Config** (that is
40
- `%APPDATA%\Claude\claude_desktop_config.json`) and add the `revit` entry,
41
- keeping any servers already there:
49
+ [Download `revit-mcp.mcpb`](https://github.com/rui-branco/revit-mcp/releases/latest/download/revit-mcp.mcpb),
50
+ then **Settings Extensions Advanced settings → Install extension…** and
51
+ pick the file. It bundles the server, its dependencies and the Revit add-in,
52
+ and runs on Claude Desktop's built-in Node runtime, so neither Node.js nor npm
53
+ is needed. **A sideloaded extension never auto-updates** — every new version
54
+ means downloading and installing the file again, which is why the plugin is
55
+ preferred.
56
+
57
+ **Claude Desktop, by hand**
58
+
59
+ **Settings → Developer → Edit Config** opens
60
+ `%APPDATA%\Claude\claude_desktop_config.json`. Add the `revit` entry, keeping
61
+ any servers already there:
42
62
 
43
63
  ```json
44
64
  {
45
65
  "mcpServers": {
46
66
  "revit": {
47
67
  "command": "npx",
48
- "args": ["-y", "github:rui-branco/revit-mcp"]
68
+ "args": ["-y", "@rui.branco/revit-mcp"]
49
69
  }
50
70
  }
51
71
  }
52
72
  ```
53
73
 
54
- Then **quit Claude Desktop from the system tray** — closing the window is not
55
- enough — and reopen it.
56
-
57
- If the server shows as failed, Claude Desktop could not find `npx` on its
58
- `PATH`. Use absolute paths instead (`where.exe npx.cmd` prints yours), or point
59
- `command` at your `node.exe` with `args` of
60
- `["C:\\path\\to\\revit-mcp\\index.js"]`.
61
-
62
- </details>
63
-
64
- #### Claude Code — one command
65
-
66
- ```bash
67
- claude mcp add revit --scope user -- npx -y github:rui-branco/revit-mcp
68
- ```
69
-
70
- `--scope user` enables it in every project; `--scope project` writes it to the
71
- repo's `.mcp.json` instead, to share with a team. Check it with `claude mcp list`.
74
+ Then quit Claude Desktop from the system tray — closing the window is not
75
+ enough — and reopen it. If the server shows as failed, Claude Desktop could not
76
+ find `npx` on its `PATH`: use an absolute path (`where.exe npx.cmd` prints
77
+ yours).
72
78
 
73
- <details>
74
- <summary><b>Other MCP clients</b></summary>
79
+ **Any other MCP client**
75
80
 
76
- Any stdio MCP client works. Use `npx -y github:rui-branco/revit-mcp` as the
77
- server command; it takes no arguments and needs no environment beyond the
78
- optional [configuration](#configuration).
81
+ Run `npx -y @rui.branco/revit-mcp` as the server command. It takes no
82
+ arguments and needs no environment beyond the optional
83
+ [configuration](#configuration).
79
84
 
80
85
  </details>
81
86
 
@@ -427,6 +432,24 @@ until the dialog is answered by hand.
427
432
  | `did not answer within Nms` | Revit is busy or showing a modal dialog. | Check `revit_diagnostics` — a dialog seen but unanswered is recorded with `answered: false`. Otherwise clear the screen in Revit and retry, or raise `REVIT_MCP_TIMEOUT`. A timed-out write may still have completed. |
428
433
  | `no active document` | Revit is on the start page or between documents. | Open a model and retry. `revit_status` deliberately tolerates this, which distinguishes it from the others. |
429
434
 
435
+ ## Privacy Policy
436
+
437
+ **This software collects no data.** It has no backend, no account, no telemetry
438
+ and no analytics, and it makes no outbound internet request of its own. Model
439
+ data is processed locally, in memory, and over loopback only.
440
+
441
+ Model content leaves your machine only if your MCP client sends it onward —
442
+ when that client is Claude, under [Anthropic's privacy
443
+ policy](https://www.anthropic.com/legal/privacy), not this one.
444
+
445
+ Written locally and never transmitted: the bridge log at
446
+ `%LOCALAPPDATA%\RevitMcpBridge\bridge.log`, the add-in under
447
+ `%APPDATA%\Autodesk\Revit\Addins\`, an in-memory 200-entry diagnostics buffer,
448
+ and any image or PDF you ask a tool to export.
449
+
450
+ Full policy — collection, storage, third-party sharing, retention and contact:
451
+ **[PRIVACY.md](PRIVACY.md)**.
452
+
430
453
  ## Security
431
454
 
432
455
  - The add-in runs **inside `Revit.exe` with full Revit API access**. It can
@@ -37,6 +37,7 @@ export function registerDetailTools(server, bridge) {
37
37
  .optional()
38
38
  .describe("Line style name, e.g. 'Thin Lines'. Unknown names fall back to the default and are reported."),
39
39
  },
40
+ { title: "Draw Detail Lines", readOnlyHint: false, destructiveHint: false },
40
41
  async ({ view_id, lines, line_style }) => {
41
42
  try {
42
43
  // The tool arguments stay snake_case like every other tool's; the bridge
@@ -74,6 +75,7 @@ export function registerDetailTools(server, bridge) {
74
75
  .min(1)
75
76
  .describe("Notes to place, all in this one call"),
76
77
  },
78
+ { title: "Add Text Notes", readOnlyHint: false, destructiveHint: false },
77
79
  async ({ view_id, notes }) => {
78
80
  try {
79
81
  const result = await bridge.call("/detail/text", { viewId: view_id, notes });
@@ -16,6 +16,7 @@ export function registerDiagnosticsTools(server, bridge) {
16
16
  "revit_diagnostics",
17
17
  "Read what the bridge suppressed: the Revit dialogs it answered automatically and the transaction warnings it resolved, oldest first, plus whether auto-dismiss is currently on. CHECK THIS AFTER EVERY BATCH OF WRITES. A silently resolved warning usually means Revit changed something you did not ask for — walls joined differently, an element deleted as a side effect of another — and this buffer is the only record of it. An empty dialogs/failures list means the writes went through cleanly.",
18
18
  {},
19
+ { title: "Read Diagnostics", readOnlyHint: true },
19
20
  async () => {
20
21
  try {
21
22
  const result = await bridge.call("/diagnostics");
@@ -40,6 +41,7 @@ export function registerDiagnosticsTools(server, bridge) {
40
41
  .default(false)
41
42
  .describe("Also empty the dialog/warning buffer that revit_diagnostics reads"),
42
43
  },
44
+ { title: "Set Dialog Auto-Dismiss", readOnlyHint: false, destructiveHint: false },
43
45
  async ({ enabled, clear }) => {
44
46
  try {
45
47
  // The tool argument stays snake_case-free and plain, but the bridge
@@ -40,6 +40,7 @@ export function registerDocumentTools(server, bridge) {
40
40
  "Rebuild the project in place: DELETES the existing save_path and the Revit backups beside it (House.0001.rvt, House.0002.rvt) and closes it in Revit first if it is open, then builds the project again at that same path. Defaults to false, which fails with FILE_EXISTS instead. A file something else still holds comes back as FILE_LOCKED — nothing is ever built under a different name.",
41
41
  ),
42
42
  },
43
+ { title: "New Project", readOnlyHint: false, destructiveHint: true },
43
44
  async ({ save_path, template_path, overwrite }) => {
44
45
  try {
45
46
  // The tool arguments stay snake_case like every other tool's, but the
@@ -69,6 +70,7 @@ export function registerDocumentTools(server, bridge) {
69
70
  "revit_save",
70
71
  "Save the active document in place. A model that has never been saved has no path to save to and comes back as NOT_SAVEABLE — use revit_save_as for that one.",
71
72
  {},
73
+ { title: "Save Project", readOnlyHint: false, destructiveHint: false },
72
74
  async () => {
73
75
  try {
74
76
  const result = await bridge.call("/document/save");
@@ -94,6 +96,7 @@ export function registerDocumentTools(server, bridge) {
94
96
  .default(false)
95
97
  .describe("Replace save_path if it already exists. Default false: an existing file fails with FILE_EXISTS."),
96
98
  },
99
+ { title: "Save Project As", readOnlyHint: false, destructiveHint: true },
97
100
  async ({ save_path, overwrite }) => {
98
101
  try {
99
102
  // The tool arguments stay snake_case like every other tool's, but the
@@ -118,6 +121,7 @@ export function registerDocumentTools(server, bridge) {
118
121
  .min(1)
119
122
  .describe("Full path of the existing .rvt file to open, e.g. 'C:\\\\Projects\\\\House.rvt'"),
120
123
  },
124
+ { title: "Open Project", readOnlyHint: false, destructiveHint: false },
121
125
  async ({ path }) => {
122
126
  try {
123
127
  const result = await bridge.call("/document/open", { path });
@@ -137,6 +141,7 @@ export function registerDocumentTools(server, bridge) {
137
141
  .default(false)
138
142
  .describe("Save the document before closing it. Default false: unsaved changes are discarded."),
139
143
  },
144
+ { title: "Close Project", readOnlyHint: false, destructiveHint: true },
140
145
  async ({ save }) => {
141
146
  try {
142
147
  const result = await bridge.call("/document/close", { save });
@@ -36,6 +36,7 @@ export function registerDocumentationTools(server, bridge) {
36
36
  .optional()
37
37
  .describe("View ids to read, all in this one call. Use this or view_id, not both."),
38
38
  },
39
+ { title: "Read View Crop", readOnlyHint: true },
39
40
  async ({ view_id, view_ids }) => {
40
41
  // Checked here rather than in the schema: a raw shape cannot express
41
42
  // "one of these two", and a call with neither must not reach Revit.
@@ -92,6 +93,7 @@ export function registerDocumentationTools(server, bridge) {
92
93
  "DEFAULTS TO TRUE. True reports the current crop and the box that would be written, and changes nothing. Pass false to apply it.",
93
94
  ),
94
95
  },
96
+ { title: "Set View Crop", readOnlyHint: false, destructiveHint: false },
95
97
  async ({ view_id, view_ids, model_bounds, dry_run }) => {
96
98
  if (view_id === undefined && view_ids === undefined) {
97
99
  return {
@@ -154,6 +156,7 @@ export function registerDocumentationTools(server, bridge) {
154
156
  "DEFAULTS TO TRUE. True reports canBeHidden and the current isHidden for every id and changes nothing. Pass false to apply it.",
155
157
  ),
156
158
  },
159
+ { title: "Hide Elements in View", readOnlyHint: false, destructiveHint: false },
157
160
  async ({ view_id, ids, hidden, dry_run }) => {
158
161
  try {
159
162
  const result = await bridge.call("/views/hide-elements", {
@@ -209,6 +212,7 @@ export function registerDocumentationTools(server, bridge) {
209
212
  "False by default: an existing file at any target path fails the whole call with FILE_EXISTS and nothing is exported.",
210
213
  ),
211
214
  },
215
+ { title: "Export PDF", readOnlyHint: false, destructiveHint: false },
212
216
  async ({ view_ids, sheet_ids, folder, filename, combine, overwrite }) => {
213
217
  // Checked here rather than in the schema: a raw shape cannot express
214
218
  // "at least one of these two", and an empty export must not reach Revit.
@@ -262,6 +266,7 @@ export function registerDocumentationTools(server, bridge) {
262
266
  .default(0)
263
267
  .describe("Body rows to skip, for paging through a long schedule"),
264
268
  },
269
+ { title: "Read Schedule Contents", readOnlyHint: true },
265
270
  async ({ schedule_id, limit, offset }) => {
266
271
  try {
267
272
  const result = await bridge.call("/schedules/read", {
@@ -282,6 +287,7 @@ export function registerDocumentationTools(server, bridge) {
282
287
  {
283
288
  sheet_id: z.number().int().describe("Sheet id from revit_list_sheets"),
284
289
  },
290
+ { title: "Measure Sheet Layout", readOnlyHint: true },
285
291
  async ({ sheet_id }) => {
286
292
  try {
287
293
  const result = await bridge.call("/sheets/layout", { sheetId: sheet_id });
@@ -310,6 +316,7 @@ export function registerDocumentationTools(server, bridge) {
310
316
  `Max sheets to return (default ${DEFAULT_QUERY_LIMIT}, hard cap ${MAX_QUERY_LIMIT} — higher values are clamped, not rejected)`,
311
317
  ),
312
318
  },
319
+ { title: "Read Browser Organization", readOnlyHint: true },
313
320
  async ({ sheet_ids, limit }) => {
314
321
  try {
315
322
  const result = await bridge.call("/sheets/browser-organization", {
@@ -353,6 +360,7 @@ export function registerDocumentationTools(server, bridge) {
353
360
  "DEFAULTS TO TRUE. True reports the current position and moves nothing. Pass false to apply it.",
354
361
  ),
355
362
  },
363
+ { title: "Move Viewport on Sheet", readOnlyHint: false, destructiveHint: false },
356
364
  async ({ viewport_id, center, label_offset, label_line_length, dry_run }) => {
357
365
  try {
358
366
  const result = await bridge.call("/sheets/set-viewport-position", {
@@ -389,6 +397,7 @@ export function registerDocumentationTools(server, bridge) {
389
397
  "DEFAULTS TO TRUE. True reports the current position and moves nothing. Pass false to apply it.",
390
398
  ),
391
399
  },
400
+ { title: "Move Schedule on Sheet", readOnlyHint: false, destructiveHint: false },
392
401
  async ({ instance_id, top_left, dry_run }) => {
393
402
  try {
394
403
  const result = await bridge.call("/sheets/set-schedule-position", {
@@ -489,6 +498,7 @@ export function registerDocumentationTools(server, bridge) {
489
498
  "DEFAULTS TO TRUE. True reports the current configuration and changes nothing. Pass false to apply it.",
490
499
  ),
491
500
  },
501
+ { title: "Configure Schedule", readOnlyHint: false, destructiveHint: false },
492
502
  async ({ schedule_id, itemized, group_by, fields, filters, grand_total, dry_run }) => {
493
503
  if (
494
504
  itemized === undefined &&
@@ -169,6 +169,7 @@ export function registerGeometryTools(server, bridge) {
169
169
  .min(1)
170
170
  .describe("One element per entry, all in this one call"),
171
171
  },
172
+ { title: "Create Geometry Elements", readOnlyHint: false, destructiveHint: false },
172
173
  async ({ category, name, type_name, material_id, material_name, comments, mark, shapes }) => {
173
174
  try {
174
175
  const result = await bridge.call("/directshape/create", {
@@ -208,6 +209,7 @@ export function registerGeometryTools(server, bridge) {
208
209
  .describe("Comments applied to every tree that does not carry its own"),
209
210
  mark: z.string().min(1).optional().describe("Mark applied to every tree that does not carry its own"),
210
211
  },
212
+ { title: "Plant Trees", readOnlyHint: false, destructiveHint: false },
211
213
  async ({
212
214
  points,
213
215
  trunk_height,
@@ -271,6 +273,7 @@ export function registerGeometryTools(server, bridge) {
271
273
  .describe("Comments applied to every run that does not carry its own"),
272
274
  mark: z.string().min(1).optional().describe("Mark applied to every run that does not carry its own"),
273
275
  },
276
+ { title: "Create Pipe Runs", readOnlyHint: false, destructiveHint: false },
274
277
  async ({ runs, category, name, type_name, material_id, material_name, comments, mark }) => {
275
278
  try {
276
279
  const result = await bridge.call("/pipes/create", {
@@ -309,6 +312,7 @@ export function registerGeometryTools(server, bridge) {
309
312
  .describe("Comments applied to every head that does not carry its own"),
310
313
  mark: z.string().min(1).optional().describe("Mark applied to every head that does not carry its own"),
311
314
  },
315
+ { title: "Place Sprinklers", readOnlyHint: false, destructiveHint: false },
312
316
  async ({ points, radius, height, name, type_name, material_id, material_name, comments, mark }) => {
313
317
  try {
314
318
  const result = await bridge.call("/sprinklers/place", {
@@ -23,6 +23,7 @@ export function registerGraphicsTools(server, bridge) {
23
23
  .optional()
24
24
  .describe("Ids of several views to read in one call. Use this or view_id, not both."),
25
25
  },
26
+ { title: "Read View Graphics", readOnlyHint: true },
26
27
  async ({ view_id, view_ids }) => {
27
28
  try {
28
29
  if (view_id === undefined && view_ids === undefined) {
@@ -82,6 +83,7 @@ export function registerGraphicsTools(server, bridge) {
82
83
  "Turn cast shadows on or off. The bridge probes the view first and reports whether it wrote the parameter (verified) or posted Revit's own command (pending and unverified).",
83
84
  ),
84
85
  },
86
+ { title: "Set View Graphics", readOnlyHint: false, destructiveHint: false },
85
87
  async ({ view_id, view_ids, style, detail_level, shadow_intensity, sunlight_intensity, shadows }) => {
86
88
  try {
87
89
  if (view_id === undefined && view_ids === undefined) {
@@ -107,6 +109,7 @@ export function registerGraphicsTools(server, bridge) {
107
109
  "revit_get_view_graphics_command_status",
108
110
  "What became of the last cast-shadows command revit_set_view_graphics posted: when it was posted, whether Revit has been idle since (which is when a posted command actually runs), and a fresh probe of the shadows parameter now, next to the one taken at the moment of posting. 'verified' is true ONLY when the parameter can be read back as an integer and matches what was asked for; when it cannot be read, verified stays false and verifiedBy is null — a posted UI command that nothing can observe is reported as unverified, not as success. Call this after a set-graphics response that said method 'posted-command'.",
109
111
  {},
112
+ { title: "View Graphics Command Status", readOnlyHint: true },
110
113
  async () => {
111
114
  try {
112
115
  const result = await bridge.call("/views/graphics-command-status");
@@ -140,6 +143,7 @@ export function registerGraphicsTools(server, bridge) {
140
143
  "Exact parameter ids the template should control, from a previous capture's response. Not to be combined with mode.",
141
144
  ),
142
145
  },
146
+ { title: "Capture View Template", readOnlyHint: false, destructiveHint: false },
143
147
  async ({ source_view_id, name, mode, parameter_ids }) => {
144
148
  try {
145
149
  const result = await bridge.call("/views/capture-template", {
@@ -179,6 +183,7 @@ export function registerGraphicsTools(server, bridge) {
179
183
  .optional()
180
184
  .describe("Allow 'assign' to detach a template a view already has. Without it, such a view fails the call instead.",),
181
185
  },
186
+ { title: "Apply View Template", readOnlyHint: false, destructiveHint: false },
182
187
  async ({ template_id, view_ids, mode, dry_run, replace }) => {
183
188
  try {
184
189
  const result = await bridge.call("/views/apply-template", {
@@ -179,6 +179,7 @@ export function registerInstallTools(server, options = {}) {
179
179
  .optional()
180
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
181
  },
182
+ { title: "Install Bridge Add-In", readOnlyHint: false, destructiveHint: false },
182
183
  async ({ revit_version, skip_build }) => {
183
184
  try {
184
185
  const result = await runInstaller(
@@ -196,6 +197,7 @@ export function registerInstallTools(server, options = {}) {
196
197
  "revit_uninstall_bridge",
197
198
  "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
  {},
200
+ { title: "Uninstall Bridge Add-In", readOnlyHint: false, destructiveHint: true },
199
201
  async () => {
200
202
  try {
201
203
  const result = await runInstaller({ uninstall: true }, options);
@@ -53,6 +53,7 @@ export function registerMaterialAppearanceTools(server, bridge) {
53
53
  "revit_get_material_appearance",
54
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
55
  MATERIAL,
56
+ { title: "Read Material Appearance", readOnlyHint: true },
56
57
  async ({ material_id, material_name }) => {
57
58
  try {
58
59
  const result = await bridge.call("/materials/appearance", {
@@ -131,6 +132,7 @@ export function registerMaterialAppearanceTools(server, bridge) {
131
132
  "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
  ),
133
134
  },
135
+ { title: "Edit Material Appearance", readOnlyHint: false, destructiveHint: false },
134
136
  async ({
135
137
  material_id,
136
138
  material_name,
@@ -58,6 +58,7 @@ export function registerMaterialTools(server, bridge) {
58
58
  "revit_list_materials",
59
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
60
  {},
61
+ { title: "List Materials", readOnlyHint: true },
61
62
  async () => {
62
63
  try {
63
64
  const result = await bridge.call("/materials", {});
@@ -110,6 +111,7 @@ export function registerMaterialTools(server, bridge) {
110
111
  "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
  ),
112
113
  },
114
+ { title: "Create Material", readOnlyHint: false, destructiveHint: false },
113
115
  async ({
114
116
  name,
115
117
  color,
@@ -168,6 +170,7 @@ export function registerMaterialTools(server, bridge) {
168
170
  .optional()
169
171
  .describe("Colour multiplied over the bitmap, as {r, g, b}, each channel 0-255"),
170
172
  },
173
+ { title: "Set Material Texture", readOnlyHint: false, destructiveHint: false },
171
174
  async ({ material_id, material_name, texture_path, scale, rotation, tint }) => {
172
175
  try {
173
176
  const result = await bridge.call("/materials/set-texture", {
@@ -192,6 +195,7 @@ export function registerMaterialTools(server, bridge) {
192
195
  material_id: z.number().int().describe("Material id from revit_list_materials or revit_create_material"),
193
196
  element_ids: z.array(z.number().int()).min(1).describe("Elements to put the material on"),
194
197
  },
198
+ { title: "Assign Material", readOnlyHint: false, destructiveHint: false },
195
199
  async ({ material_id, element_ids }) => {
196
200
  try {
197
201
  const result = await bridge.call("/materials/assign", {
@@ -209,6 +213,7 @@ export function registerMaterialTools(server, bridge) {
209
213
  "revit_create_wall_type",
210
214
  "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
215
  HOST_TYPE,
216
+ { title: "Create Wall Type", readOnlyHint: false, destructiveHint: false },
212
217
  async ({ name, based_on_type_name, thickness, material_id, material_name }) => {
213
218
  try {
214
219
  const result = await bridge.call("/walltypes/create", {
@@ -229,6 +234,7 @@ export function registerMaterialTools(server, bridge) {
229
234
  "revit_create_floor_type",
230
235
  "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
236
  HOST_TYPE,
237
+ { title: "Create Floor Type", readOnlyHint: false, destructiveHint: false },
232
238
  async ({ name, based_on_type_name, thickness, material_id, material_name }) => {
233
239
  try {
234
240
  const result = await bridge.call("/floortypes/create", {
@@ -43,6 +43,7 @@ export function registerModelTools(server, bridge) {
43
43
  .optional()
44
44
  .describe("Level the toposolid is hosted on. Omit it to use the lowest level in the document."),
45
45
  },
46
+ { title: "Create Site Surface", readOnlyHint: false, destructiveHint: false },
46
47
  async ({ points, type_name, level }) => {
47
48
  try {
48
49
  const result = await bridge.call("/toposolid/create", {
@@ -72,6 +73,7 @@ export function registerModelTools(server, bridge) {
72
73
  .optional()
73
74
  .describe("Id of the toposolid to flatten. Only needed when the document has more than one."),
74
75
  },
76
+ { title: "Flatten Site Surface Region", readOnlyHint: false, destructiveHint: true },
75
77
  async ({ points, elevation, toposolid_id }) => {
76
78
  try {
77
79
  const result = await bridge.call("/toposolid/flatten", {
@@ -111,6 +113,7 @@ export function registerModelTools(server, bridge) {
111
113
  "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
114
  ),
113
115
  },
116
+ { title: "Create Floor", readOnlyHint: false, destructiveHint: false },
114
117
  async ({ level, boundary, type_name, structural, offset }) => {
115
118
  try {
116
119
  const result = await bridge.call("/floors/create", {
@@ -138,6 +141,7 @@ export function registerModelTools(server, bridge) {
138
141
  .describe("Full paths of .rfa files to load. Pass the whole batch in one call."),
139
142
  path: z.string().min(1).optional().describe("A single .rfa path, as a shorthand for paths"),
140
143
  },
144
+ { title: "Load Families", readOnlyHint: false, destructiveHint: false },
141
145
  async ({ paths, path }) => {
142
146
  try {
143
147
  if (!paths && !path) {
@@ -172,6 +176,7 @@ export function registerModelTools(server, bridge) {
172
176
  "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
177
  ),
174
178
  },
179
+ { title: "List Family Types", readOnlyHint: true },
175
180
  async ({ category, family_name }) => {
176
181
  try {
177
182
  const result = await bridge.call("/families/symbols", {
@@ -215,6 +220,7 @@ export function registerModelTools(server, bridge) {
215
220
  "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
221
  ),
217
222
  },
223
+ { title: "Place Families", readOnlyHint: false, destructiveHint: false },
218
224
  async ({ symbol_id, level, points, z: zOffset, rotation }) => {
219
225
  try {
220
226
  const result = await bridge.call("/families/place", {
@@ -264,6 +270,7 @@ export function registerModelTools(server, bridge) {
264
270
  "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
271
  ),
266
272
  },
273
+ { title: "Place Doors and Windows", readOnlyHint: false, destructiveHint: false },
267
274
  async ({ symbol_id, points, host_wall_id, level, sill_height }) => {
268
275
  try {
269
276
  // The tool arguments stay snake_case like every other tool's; the bridge
@@ -63,6 +63,7 @@ export function registerParameterTools(server, bridge) {
63
63
  .optional()
64
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
65
  },
66
+ { title: "Create Project Parameter", readOnlyHint: false, destructiveHint: false },
66
67
  async ({ name, category, categories, type, group, instance }) => {
67
68
  // Checked here rather than in the schema: a raw shape cannot express
68
69
  // "one of these two", and a call with neither must not reach Revit.
@@ -120,6 +121,7 @@ export function registerParameterTools(server, bridge) {
120
121
  .min(1)
121
122
  .describe("Sheet/name/value triples, all in this one call"),
122
123
  },
124
+ { title: "Set Sheet Parameters", readOnlyHint: false, destructiveHint: false },
123
125
  async ({ values }) => {
124
126
  try {
125
127
  // The tool arguments stay snake_case like every other tool's; the bridge
@@ -50,6 +50,7 @@ export function registerQualityTools(server, bridge) {
50
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
51
  ),
52
52
  },
53
+ { title: "Inspect Elements", readOnlyHint: true },
53
54
  async ({ ids, include_parameters, include_geometry }) => {
54
55
  try {
55
56
  // The tool arguments stay snake_case like every other tool's, but the
@@ -88,6 +89,7 @@ export function registerQualityTools(server, bridge) {
88
89
  "true (the default) measures and reports what the move would do without changing the model. Pass false to actually move.",
89
90
  ),
90
91
  },
92
+ { title: "Move Elements", readOnlyHint: false, destructiveHint: false },
91
93
  async ({ ids, translation, dry_run }) => {
92
94
  try {
93
95
  const result = await bridge.call("/elements/move", {
@@ -129,6 +131,7 @@ export function registerQualityTools(server, bridge) {
129
131
  "true (the default) runs the CanBeExcavatedBy preflight and reports the current volume without changing the model. Pass false to actually excavate.",
130
132
  ),
131
133
  },
134
+ { title: "Excavate Site Surface", readOnlyHint: false, destructiveHint: false },
132
135
  async ({ toposolid_id, ids, dry_run }) => {
133
136
  try {
134
137
  const result = await bridge.call("/toposolid/excavate", {
@@ -165,6 +168,7 @@ export function registerQualityTools(server, bridge) {
165
168
  .default(0)
166
169
  .describe("Warnings to skip, for paging through a noisy model"),
167
170
  },
171
+ { title: "Read Model Warnings", readOnlyHint: true },
168
172
  async ({ limit, offset }) => {
169
173
  try {
170
174
  const result = await bridge.call("/document/warnings", {
@@ -185,6 +189,7 @@ export function registerQualityTools(server, bridge) {
185
189
  "revit_list_view_templates",
186
190
  "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
191
  {},
192
+ { title: "List View Templates", readOnlyHint: true },
188
193
  async () => {
189
194
  try {
190
195
  const result = await bridge.call("/views/templates");
package/lib/tools/read.js CHANGED
@@ -19,6 +19,7 @@ export function registerReadTools(server, bridge) {
19
19
  "revit_status",
20
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
21
  {},
22
+ { title: "Revit Status", readOnlyHint: true },
22
23
  async () => {
23
24
  try {
24
25
  const result = await bridge.call("/status");
@@ -33,6 +34,7 @@ export function registerReadTools(server, bridge) {
33
34
  "revit_list_levels",
34
35
  "List the levels in the active document: id, name and elevation (Revit internal units, decimal feet).",
35
36
  {},
37
+ { title: "List Levels", readOnlyHint: true },
36
38
  async () => {
37
39
  try {
38
40
  const result = await bridge.call("/levels");
@@ -47,6 +49,7 @@ export function registerReadTools(server, bridge) {
47
49
  "revit_list_categories",
48
50
  "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
51
  {},
52
+ { title: "List Categories", readOnlyHint: true },
50
53
  async () => {
51
54
  try {
52
55
  const result = await bridge.call("/categories");
@@ -85,6 +88,7 @@ export function registerReadTools(server, bridge) {
85
88
  .default(0)
86
89
  .describe("Rows to skip, for paging through a large result"),
87
90
  },
91
+ { title: "Find Elements", readOnlyHint: true },
88
92
  async ({ category, level, type_name, limit, offset }) => {
89
93
  try {
90
94
  // The tool argument stays snake_case like every other tool's, but the
@@ -116,6 +120,7 @@ export function registerReadTools(server, bridge) {
116
120
  .optional()
117
121
  .describe("Parameter names to read, e.g. ['Comments', 'Mark', 'Unconnected Height']"),
118
122
  },
123
+ { title: "Read Elements", readOnlyHint: true },
119
124
  async ({ ids, params }) => {
120
125
  try {
121
126
  const result = await bridge.call("/elements", { ids, params });
@@ -130,6 +135,7 @@ export function registerReadTools(server, bridge) {
130
135
  "revit_get_selection",
131
136
  "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
137
  {},
138
+ { title: "Read Current Selection", readOnlyHint: true },
133
139
  async () => {
134
140
  try {
135
141
  const result = await bridge.call("/selection");
@@ -15,6 +15,7 @@ export function registerReloadTools(server, bridge) {
15
15
  "revit_reload_bridge",
16
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
17
  {},
18
+ { title: "Reload Bridge Logic", readOnlyHint: false, destructiveHint: false },
18
19
  async () => {
19
20
  try {
20
21
  const result = await bridge.call("/reload");
@@ -10,6 +10,7 @@ export function registerSheetCollectionTools(server, bridge) {
10
10
  "revit_list_sheet_collections",
11
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
12
  {},
13
+ { title: "List Sheet Collections", readOnlyHint: true },
13
14
  async () => {
14
15
  try {
15
16
  const result = await bridge.call("/sheets/collections");
@@ -46,6 +47,7 @@ export function registerSheetCollectionTools(server, bridge) {
46
47
  .optional()
47
48
  .describe("Defaults to true: report the plan without writing. Pass false to apply it."),
48
49
  },
50
+ { title: "Group Sheets into Collections", readOnlyHint: false, destructiveHint: false },
49
51
  async ({ collections, dry_run }) => {
50
52
  try {
51
53
  // Tool arguments stay snake_case; the bridge reads sheetIds / dryRun.
@@ -11,6 +11,7 @@ export function registerSheetTools(server, bridge) {
11
11
  "revit_list_titleblocks",
12
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
13
  {},
14
+ { title: "List Title Blocks", readOnlyHint: true },
14
15
  async () => {
15
16
  try {
16
17
  const result = await bridge.call("/titleblocks");
@@ -25,6 +26,7 @@ export function registerSheetTools(server, bridge) {
25
26
  "revit_list_sheets",
26
27
  "List the sheets in the active document: id, sheet number and name, ordered by sheet number.",
27
28
  {},
29
+ { title: "List Sheets", readOnlyHint: true },
28
30
  async () => {
29
31
  try {
30
32
  const result = await bridge.call("/sheets");
@@ -59,6 +61,7 @@ export function registerSheetTools(server, bridge) {
59
61
  "Title block family type id from revit_list_titleblocks. Omit it to use the first one loaded.",
60
62
  ),
61
63
  },
64
+ { title: "Create Sheets", readOnlyHint: false, destructiveHint: false },
62
65
  async ({ sheets, title_block_id }) => {
63
66
  try {
64
67
  // The tool argument stays snake_case like every other tool's, but the
@@ -32,6 +32,7 @@ export function registerTitleblockTools(server, bridge) {
32
32
  .int()
33
33
  .describe("Id of the loaded title block TYPE (a FamilySymbol), from revit_list_titleblocks"),
34
34
  },
35
+ { title: "Inspect Title Block Family", readOnlyHint: true },
35
36
  async ({ symbol_id }) => {
36
37
  try {
37
38
  // Tool arguments stay snake_case; the bridge reads symbolId.
@@ -119,6 +120,7 @@ export function registerTitleblockTools(server, bridge) {
119
120
  "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
  ),
121
122
  },
123
+ { title: "Edit Title Block Family", readOnlyHint: false, destructiveHint: true },
122
124
  async ({
123
125
  symbol_id,
124
126
  expected_family_name,
@@ -21,6 +21,7 @@ export function registerViewTools(server, bridge) {
21
21
  "revit_list_views",
22
22
  "List the non-template views in the document: id, name, view type, and whether each is already placed on a sheet. A view with a frame also carries viewDirection, rightDirection and upDirection as {x,y,z} — viewDirection is Revit's direction towards the VIEWER, so a section looking north reports {x:0,y:-1,z:0}. Sheets themselves are not listed — use revit_list_sheets for those. A view that is already on a sheet cannot be placed on another one; duplicate it first with revit_duplicate_view.",
23
23
  {},
24
+ { title: "List Views", readOnlyHint: true },
24
25
  async () => {
25
26
  try {
26
27
  const result = await bridge.call("/views");
@@ -51,6 +52,7 @@ export function registerViewTools(server, bridge) {
51
52
  .optional()
52
53
  .describe("View scale denominator, e.g. 100 for 1:100. Omit it to keep the type's default."),
53
54
  },
55
+ { title: "Create Plan View", readOnlyHint: false, destructiveHint: false },
54
56
  async ({ level, name, view_family_type, scale }) => {
55
57
  try {
56
58
  // The tool arguments stay snake_case like every other tool's; the bridge
@@ -80,6 +82,7 @@ export function registerViewTools(server, bridge) {
80
82
  .optional()
81
83
  .describe("View scale denominator, e.g. 20 for 1:20. Omit it to keep the type's default."),
82
84
  },
85
+ { title: "Create Drafting View", readOnlyHint: false, destructiveHint: false },
83
86
  async ({ name, scale }) => {
84
87
  try {
85
88
  const result = await bridge.call("/views/create-drafting", { name, scale });
@@ -122,6 +125,7 @@ export function registerViewTools(server, bridge) {
122
125
  .optional()
123
126
  .describe("View scale denominator, e.g. 50 for 1:50. Omit it to keep the type's default."),
124
127
  },
128
+ { title: "Create Section View", readOnlyHint: false, destructiveHint: false },
125
129
  async ({ name, origin, direction, width, height, depth, scale }) => {
126
130
  try {
127
131
  const result = await bridge.call("/views/create-section", {
@@ -144,6 +148,7 @@ export function registerViewTools(server, bridge) {
144
148
  "revit_list_legends",
145
149
  "List the legend views in the document: id, name and scale. Read-only, and that is the point — Revit's API cannot author the first legend in a document, so this is the set revit_create_legend has to duplicate from. An empty list means a legend has to be made once in the Revit UI (View tab > Legends > Legend), or come from the template, before any legend can be created from here.",
146
150
  {},
151
+ { title: "List Legends", readOnlyHint: true },
147
152
  async () => {
148
153
  try {
149
154
  const result = await bridge.call("/views/legends");
@@ -171,6 +176,7 @@ export function registerViewTools(server, bridge) {
171
176
  .optional()
172
177
  .describe("View scale denominator, e.g. 50 for 1:50. Omit it to keep the source legend's scale."),
173
178
  },
179
+ { title: "Create Legend", readOnlyHint: false, destructiveHint: false },
174
180
  async ({ name, from_legend_id, scale }) => {
175
181
  try {
176
182
  // The tool arguments stay snake_case like every other tool's; the bridge
@@ -211,6 +217,7 @@ export function registerViewTools(server, bridge) {
211
217
  .optional()
212
218
  .describe("View scale denominator, e.g. 100 for 1:100. Ignored on a perspective view."),
213
219
  },
220
+ { title: "Create 3D View", readOnlyHint: false, destructiveHint: false },
214
221
  async ({ name, eye, target, perspective, scale }) => {
215
222
  try {
216
223
  const result = await bridge.call("/views/create-3d", {
@@ -250,6 +257,7 @@ export function registerViewTools(server, bridge) {
250
257
  "Not handled here. Send it to revit_set_view_graphics, which probes whether the shadows parameter is writable on that view and says which route it took. Passing it here fails the call rather than silently doing nothing.",
251
258
  ),
252
259
  },
260
+ { title: "Set View Display Style", readOnlyHint: false, destructiveHint: false },
253
261
  async ({ view_id, style, detail_level, shadows }) => {
254
262
  try {
255
263
  const result = await bridge.call("/views/set-style", {
@@ -295,6 +303,7 @@ export function registerViewTools(server, bridge) {
295
303
  "Full path of the backdrop image, required with kind 'image'. Revit reads it off disk every time it draws the view, so the file has to stay there.",
296
304
  ),
297
305
  },
306
+ { title: "Set View Background", readOnlyHint: false, destructiveHint: false },
298
307
  async ({ view_id, kind, sky_color, horizon_color, ground_color, image_path }) => {
299
308
  try {
300
309
  // The tool arguments stay snake_case like every other tool's; the bridge
@@ -330,6 +339,7 @@ export function registerViewTools(server, bridge) {
330
339
  .optional()
331
340
  .describe("True (the default) hides them; false brings them back."),
332
341
  },
342
+ { title: "Hide Categories in View", readOnlyHint: false, destructiveHint: false },
333
343
  async ({ view_id, categories, hidden }) => {
334
344
  try {
335
345
  const result = await bridge.call("/views/hide-categories", {
@@ -398,6 +408,7 @@ export function registerViewTools(server, bridge) {
398
408
  "DEFAULTS TO TRUE. True reports each category's current override and the merged override that would be written, and changes nothing. Pass false to apply it.",
399
409
  ),
400
410
  },
411
+ { title: "Override Category Graphics", readOnlyHint: false, destructiveHint: false },
401
412
  async ({ view_id, overrides, dry_run }) => {
402
413
  const rows = overrides.map((row) => ({
403
414
  category: row.category,
@@ -471,6 +482,7 @@ export function registerViewTools(server, bridge) {
471
482
  .optional()
472
483
  .describe("Time as HH:MM on a 24-hour clock, e.g. '15:30'. Puts the view in Still Image mode. Not to be combined with azimuth/altitude."),
473
484
  },
485
+ { title: "Set View Sun Position", readOnlyHint: false, destructiveHint: false },
474
486
  async ({ view_id, azimuth, altitude, date, time }) => {
475
487
  // Checked here rather than in the schema: a raw shape cannot express
476
488
  // "one of these two groups", and a call with neither must not reach Revit.
@@ -540,6 +552,7 @@ export function registerViewTools(server, bridge) {
540
552
  .optional()
541
553
  .describe("PNG (the default), JPEG, JPEGLossless, JPEGMedium, JPEGSmallest, BMP, TIFF or TARGA"),
542
554
  },
555
+ { title: "Export View Image", readOnlyHint: false, destructiveHint: false },
543
556
  async ({ view_id, view_ids, path, width, height, format }) => {
544
557
  try {
545
558
  if (view_id === undefined && !view_ids) {
@@ -573,6 +586,7 @@ export function registerViewTools(server, bridge) {
573
586
  .optional()
574
587
  .describe("Revit's ViewDuplicateOption. Defaults to Duplicate."),
575
588
  },
589
+ { title: "Duplicate View", readOnlyHint: false, destructiveHint: false },
576
590
  async ({ view_id, name, detailing }) => {
577
591
  try {
578
592
  const result = await bridge.call("/views/duplicate", {
@@ -607,6 +621,7 @@ export function registerViewTools(server, bridge) {
607
621
  .positive()
608
622
  .describe("View scale denominator, e.g. 100 for 1:100. Revit's range is 1 to 24000."),
609
623
  },
624
+ { title: "Set View Scale", readOnlyHint: false, destructiveHint: false },
610
625
  async ({ view_id, view_ids, scale }) => {
611
626
  // Checked here rather than in the schema: a raw shape cannot express
612
627
  // "one of these two", and a call with neither must not reach Revit.
@@ -659,6 +674,7 @@ export function registerViewTools(server, bridge) {
659
674
  "DEFAULTS TO TRUE. True reports the view's current size and the multiplier asked for, and changes nothing. Pass false to apply it.",
660
675
  ),
661
676
  },
677
+ { title: "Resize Perspective View", readOnlyHint: false, destructiveHint: false },
662
678
  async ({ view_id, multiplier, dry_run }) => {
663
679
  try {
664
680
  const result = await bridge.call("/views/scale-perspective-crop", {
@@ -695,6 +711,7 @@ export function registerViewTools(server, bridge) {
695
711
  .min(1)
696
712
  .describe("Placements to make, all in this one call"),
697
713
  },
714
+ { title: "Place Views on Sheets", readOnlyHint: false, destructiveHint: false },
698
715
  async ({ placements }) => {
699
716
  try {
700
717
  // The tool arguments stay snake_case like every other tool's; the bridge
@@ -734,6 +751,7 @@ export function registerViewTools(server, bridge) {
734
751
  .optional()
735
752
  .describe("View scale denominator. Schedules have no meaningful scale; omit it unless you know otherwise."),
736
753
  },
754
+ { title: "Create Schedule", readOnlyHint: false, destructiveHint: false },
737
755
  async ({ category, name, fields, scale }) => {
738
756
  try {
739
757
  const result = await bridge.call("/schedules/create", { category, name, fields, scale });
@@ -22,6 +22,7 @@ export function registerWriteTools(server, bridge) {
22
22
  .min(1)
23
23
  .describe("Levels to create"),
24
24
  },
25
+ { title: "Create Levels", readOnlyHint: false, destructiveHint: false },
25
26
  async ({ levels }) => {
26
27
  try {
27
28
  const result = await bridge.call("/levels/create", { levels });
@@ -52,6 +53,7 @@ export function registerWriteTools(server, bridge) {
52
53
  .min(1)
53
54
  .describe("Wall centrelines in plan, in feet. One wall per curve."),
54
55
  },
56
+ { title: "Create Walls", readOnlyHint: false, destructiveHint: false },
55
57
  async ({ level, wall_type, height, curves }) => {
56
58
  try {
57
59
  // The tool argument stays snake_case like every other tool's, but the
@@ -86,6 +88,7 @@ export function registerWriteTools(server, bridge) {
86
88
  "Revit's own parameter id, for when the name matches more than one: negative for a built-in (e.g. -1002062 SCHEDULE_LEVEL_PARAM), positive for a shared or project parameter. Must be a parameter of the name given. Only needed when the name is ambiguous.",
87
89
  ),
88
90
  },
91
+ { title: "Set Parameters", readOnlyHint: false, destructiveHint: false },
89
92
  async ({ ids, name, value, parameter_id }) => {
90
93
  try {
91
94
  // Left out entirely when it was not given: the bridge only asks which
@@ -110,6 +113,7 @@ export function registerWriteTools(server, bridge) {
110
113
  {
111
114
  ids: z.array(z.number().int()).min(1).describe("Element ids to delete"),
112
115
  },
116
+ { title: "Delete Elements", readOnlyHint: false, destructiveHint: true },
113
117
  async ({ ids }) => {
114
118
  try {
115
119
  const result = await bridge.call("/elements/delete", { ids });
package/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "manifest_version": "0.2",
3
3
  "name": "revit-mcp",
4
4
  "display_name": "Revit",
5
- "version": "1.0.0",
5
+ "version": "1.0.2",
6
6
  "description": "Query and edit the Autodesk Revit model open on your desktop.",
7
7
  "long_description": "Connects Claude to the Revit model currently open on this Windows machine through a bundled C# bridge add-in. Query levels and elements, read and write parameters, model geometry, and produce a full drawing set: sheets, views, schedules and PDF exports. Requires Autodesk Revit 2025, 2026 or 2027, and a one-time bridge install run from inside Claude.",
8
8
  "author": {
@@ -412,5 +412,8 @@
412
412
  "runtimes": {
413
413
  "node": ">=18.0.0"
414
414
  }
415
- }
415
+ },
416
+ "privacy_policies": [
417
+ "https://github.com/rui-branco/revit-mcp/blob/main/PRIVACY.md"
418
+ ]
416
419
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rui.branco/revit-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "MCP server for Autodesk Revit: read and edit the open model from an MCP client, via a bundled C# bridge add-in that runs inside Revit",
5
5
  "keywords": [
6
6
  "revit",
@@ -32,7 +32,8 @@
32
32
  "revit-bridge/dist/",
33
33
  "README.md",
34
34
  "LICENSE",
35
- "manifest.json"
35
+ "manifest.json",
36
+ "PRIVACY.md"
36
37
  ],
37
38
  "engines": {
38
39
  "node": ">=18"