{
  "server": "revit",
  "generated": "2026-08-30",
  "generated_from": "Revit 2026",
  "tool_count": 128,
  "tools": [
    {
      "name": "revit_apply_view_template",
      "description": "Apply one view template to many views at once, or clear the template from them.\n\nRevit is asked whether the template suits each view before it is applied, so\nincompatible views - a plan template on a section, say - come back as blocked\nrather than throwing. Revit's browser pseudo-views are never included. The\nwhole batch is one undo step.\n\nReturns:\n    str: JSON with {dry_run, template, cleared, applied|would_apply, skipped,\n         failed, failures, views: [{view_id, name, view_type,\n         current_template_id, blocked}]}"
    },
    {
      "name": "revit_assign_sheet_collection",
      "description": "Group sheets into a named sheet collection. REVIT 2025 AND NEWER ONLY.\n\nSheet collections were introduced in Revit 2025. On Revit 2024 this action is\nnot registered at all and the bridge answers 'Unknown action' - check the\nconnected version with revit_list_instances if you are unsure which Revit you\nare talking to.\n\nReturns:\n    str: JSON with {collection_name, collection_id, assigned_count, failed,\n         failures, sheets}"
    },
    {
      "name": "revit_audit_cad_imports",
      "description": "Find imported (not linked) CAD, exploded DWG geometry, and CAD sitting in 3D.\n\nA classic cause of file bloat: an imported DWG is embedded permanently and drags\nits line patterns and text styles into the project; CAD left in model space\nrenders in every 3D view; exploded DWG turns every line into an individual Revit\nelement that can never be removed as a unit.\n\nReturns:\n    str: JSON with {total_cad_instances, imported_not_linked, linked,\n                    in_model_space_visible_in_3d, exploded_curve_elements,\n                    exploded_by_import_style, import_line_style_subcategories,\n                    cad_link_files, cad_instances: [{id, name, is_linked,\n                    view_specific, owner_view, pinned, problems}], limitations}"
    },
    {
      "name": "revit_audit_in_place_families",
      "description": "List in-place families with counts by category, and their instances.\n\nIn-place families cannot be reused, are re-evaluated individually on every\nregeneration, and schedule badly. They are one of the most reliable predictors\nof a slow model. Definitions with no instances still load with the file.\n\nReturns:\n    str: JSON with {total_in_place_instances, in_place_family_definitions,\n                    definitions_with_no_instances, by_category,\n                    families: [{family_id, family_name, category, instances}],\n                    instances: [{id, name, category, family_name, level, workset}]}"
    },
    {
      "name": "revit_audit_links",
      "description": "Audit every RVT, IFC, CAD and point-cloud link in the model.\n\nReports name, kind, status and load state, path and whether it is absolute or\nrelative, whether the instance is pinned, which workset it is on, how many\ninstances are placed, and the file's size and modification time on disk where\nthe link is a local file.\n\nNote: the \"last written\" time is the file's timestamp on disk, not Revit's\ninternal \"last saved by\" record - Revit does not expose that without opening\nthe linked file. Cloud-hosted links have no local path.\n\nReturns:\n    str: JSON with {total_links, by_kind, not_loaded, absolute_paths,\n                    unplaced_link_types, links: [...], issues: [...], limitations}"
    },
    {
      "name": "revit_audit_naming",
      "description": "Check views, sheets, families, types, worksets and levels against naming rules.\n\nRules are caller-supplied regex or glob-style patterns, so this enforces the\nclient's standard rather than an opinion baked into the tool. Every violation\ncomes back with the current name and which rule it broke.\n\nReturns:\n    str: JSON with {rules_applied, rule_errors, items_in_scope, total_violations,\n                    returned, truncated, violations_by_rule, violations_by_target,\n                    violations: [{target, field, id, current_name, rule, reason}]}"
    },
    {
      "name": "revit_audit_view_usage",
      "description": "Find views not on any sheet, sheets with no views, duplicated view names and crop-disabled views.\n\nRevit enforces unique view names, so the practical duplicate signal is the\ncopy-suffix heuristic (\"Copy 1\", \"(2)\", \"- Copy\"), which is reported separately.\nA working view deliberately kept off sheets is not a defect - this is a\nhousekeeping list.\n\nReturns:\n    str: JSON with {total_views, view_templates, total_sheets, views_not_on_sheets,\n                    views_not_on_sheets_by_type, sheets_with_no_views,\n                    duplicate_view_names, copy_suffixed_view_names,\n                    views_with_crop_disabled, views_per_sheet, not_on_sheets,\n                    empty_sheets, duplicate_names, copy_suffixed_names, crop_disabled}"
    },
    {
      "name": "revit_audit_worksets",
      "description": "Find elements on a workset that does not match a category-to-workset convention.\n\nAlso flags anything left on the default workset, which is almost always an\nelement nobody placed deliberately. Non-workshared models return a clear note\nrather than an error.\n\nReturns:\n    str: JSON with {workshared, default_workset, elements_scanned,\n                    elements_on_default_workset, total_violations, truncated,\n                    violations_by_category, categories_not_covered_by_any_rule,\n                    violations: [{id, category, current_workset, expected_workset, rule}]}"
    },
    {
      "name": "revit_batch_create_sheets",
      "description": "Create a whole sheet set from an inline list or a CSV, as one undoable operation.\n\nCSV columns: 'number' (or 'sheet_number') is required; 'name' and\n'titleblock' are optional; every other column becomes a sheet parameter\nnamed by its header exactly as typed.\n\nThe entire set is validated before the model is touched - empty numbers,\nduplicates within the batch, numbers already used in the model, and unknown\ntitleblocks all fail the whole call rather than leaving a half-built set.\ndry_run=true reports that validation plus every row.\n\nAll the sheets are created inside one transaction group, so one Ctrl+Z in\nRevit removes the entire set.\n\nReturns:\n    str: JSON with {dry_run, source, created_count|would_create, blocked,\n         failures, sheets: [{sheet_id, sheet_number, sheet_name, titleblock,\n         parameter_warnings}]}"
    },
    {
      "name": "revit_batch_set_parameters",
      "description": "Set one or many parameters across many elements in ONE transaction and ONE undo step.\n\nThis is the tool to reach for whenever more than one element needs the same\nedit. It replaces N calls to revit_set_parameter, which produced N separate\ntransactions and N undo steps - a user could not back that out of a model.\n\nSelect either by explicit ids or by an inline filter, e.g.\nfilter={\"category\": \"Doors\", \"level\": \"Level 1\"}.\n\nLengths: pass units on the assignment, e.g.\n{\"name\": \"Sill Height\", \"value\": 900, \"units\": \"mm\"}. A bare number with no\nunits is written as Revit internal units (decimal feet) and the response\nsays so. Alternatively set display_string=true and pass text Revit's UI\nwould accept.\n\nArgs:\n    params.element_ids / params.filter: what to act on\n    params.parameters: the assignments\n    params.dry_run: preview only; no transaction is opened\n\nReturns:\n    str: JSON with {selector, elements_matched, summary{set,unchanged,skipped,failed},\n         results[{element_id, parameter, status, current_value, proposed_value, reason}],\n         problems, undo}"
    },
    {
      "name": "revit_capture_view_image",
      "description": "Export a Revit view or sheet to PNG and return it as an image you can actually see.\n\nThis is how you look at the model rather than only reading about it. Use it\nto check what a view contains, to confirm a change landed, or to show the\nuser what you did. Combine with revit_zoom_to_elements to frame something\nfirst, or with revit_isolate_elements to strip a busy view down.\n\nArgs:\n    params.view_id / params.view_name: View or sheet to capture. Omit for the active view.\n    params.pixel_width: Width in pixels, 100-2400 (default 1200)\n    params.export_range: 'view' for the whole view without disturbing Revit\n        (default), 'visible' for just what is on screen right now\n    params.output_path: Optional extra path to save the PNG to\n    params.return_image: Set False for the metadata only, no image\n\nReturns:\n    The PNG as image content the model can see, plus a JSON block with\n    {file_path, view, pixel_width, export_range, file_bytes,\n    estimated_base64_bytes, oversize, warning}."
    },
    {
      "name": "revit_change_element_type",
      "description": "Swap the family type on many elements at once, in one undo step.\n\nEach element is checked against the target type first - same category, and\nRevit's own IsValidType - so an element that cannot take the type is\nreported and skipped rather than failing the batch. Elements already on the\ntarget type come back as 'unchanged'.\n\nArgs:\n    params.element_ids / params.filter: what to retype\n    params.target_type_id, or target_type_name (+ target_family_name)\n    params.dry_run: preview only\n\nReturns:\n    str: JSON with {target_type, would_change/changed, results[{element_id,\n         status, from_type, to_type, reason}], undo}"
    },
    {
      "name": "revit_check_model_health",
      "description": "Run a quick model health check: warnings, errors, unplaced rooms, total elements.\n\nFor a full standards/QA report (warnings by type, in-place families, CAD imports,\nlinks, purgeable content, view usage, worksets) use revit_check_model_health_v2.\n\nReturns:\n    str: JSON with {total_elements, warning_count, error_count, unplaced_rooms,\n                    is_workshared, health_score}"
    },
    {
      "name": "revit_check_model_health_v2",
      "description": "Full model health and standards report - the one to run before issuing a model.\n\nCovers: element counts; warnings grouped by type with the worst offending types and\nelements; error-severity count; unplaced and unbounded rooms; in-place families;\nimported (rather than linked) CAD and exploded DWG geometry; purgeable content;\nviews vs. views not on sheets vs. sheets; design options; worksets and elements\nleft on the default workset; model file size; links and their load state; and\nduplicate Mark values.\n\nEvery finding carries a severity (info/low/medium/high) and a one-line explanation\nof why it matters. Severities are triage heuristics, not a published standard.\n\nrevit_check_model_health (v1) is unchanged and still available for the quick\nfour-number version.\n\nReturns:\n    str: JSON with {document, health_score, findings_by_severity, metrics,\n                    findings: [{id, title, severity, count, why_it_matters, detail}],\n                    notes, elapsed_ms}"
    },
    {
      "name": "revit_check_structural_supports",
      "description": "Find structural framing with unsupported ends and load paths that never reach the ground.\n\nFor each framing member, both ends of its location line are tested for a\nsupporting element within the tolerance; support by another framing member is\nthen followed to see whether the chain ever reaches a column, wall, floor or\nfoundation.\n\nThis is a geometric connectivity check, NOT a structural analysis: it says\nnothing about capacity or stability. Supports are matched against element\nbounding boxes, so a beam end inside a large neighbour's box can read as\nsupported. A cantilever legitimately has one free end. Linked models are not\nconsidered, so structure in a link reads as absent. The response repeats these\nlimits and reports whether an analytical model exists.\n\nReturns:\n    str: JSON with {framing_members, members_scanned, members_ok,\n                    both_ends_unsupported, one_end_unsupported,\n                    load_path_not_resolved, analytical_model_present,\n                    members: [{id, type_name, level, verdict, explanation,\n                               end_a, end_b}], method, limitations}"
    },
    {
      "name": "revit_clear_selection",
      "description": "Clear the current Revit selection.\n\nReturns:\n    str: JSON with {cleared, selected_count}"
    },
    {
      "name": "revit_color_by_parameter",
      "description": "Colour every element of a Revit category by the distinct values of one parameter, and return the legend.\n\nThe classic \"show me the model by workset / phase / type\" move. Returns\nvalue -> colour -> count, so you can describe what the user is now looking\nat, and capture the view to show them.\n\ndry_run=True returns the same legend without touching the view, which is\nalso the cheap way to ask how many distinct values a parameter actually has\nbefore recolouring anything. reset=True clears the colours again.\n\nArgs:\n    params.category: Category to colour, e.g. 'Walls'\n    params.parameter_name: Parameter to colour by. Instance and type\n        parameters both work, plus 'Workset', 'Level', 'Type', 'Category'\n        and 'Phase Created'.\n    params.view_id / params.view_name: Which view. Omit for the active view.\n    params.max_values: How many values get their own colour (rest go grey)\n    params.limit: Max elements to process\n    params.visible_in_view_only: Only elements visible in the target view\n    params.reset: Clear the colours instead of applying them\n    params.dry_run: Return the legend without changing the view\n\nReturns:\n    str: JSON with {dry_run, coloured_elements, coloured|would_color:\n         {view, category, parameter_name, element_count, distinct_values,\n         legend: [{value, color, rgb, count, sample_element_ids}, ...]}}"
    },
    {
      "name": "revit_copy_elements",
      "description": "Copy elements by a translation vector, or onto other levels, in one undo step.\n\nTwo modes:\n  translation - copy by {dx, dy, dz} in `units`\n  to_levels   - copy onto each named level. Each copy is re-pointed at the\n                target level through its own level parameter where it has a\n                settable one, so a wall keeps its base offset exactly as\n                Revit's own Copy to Levels does; anything without one is\n                translated by the elevation difference instead. The response\n                says which route each element took.\n\nArgs:\n    params.element_ids / params.filter: what to copy\n    params.units + params.translation, or params.to_levels\n    params.dry_run: preview only\n\nReturns:\n    str: JSON with {mode, translation, to_levels, created_count, created_ids,\n         per_element, undo}"
    },
    {
      "name": "revit_create_dimension",
      "description": "Create a linear dimension between grids, levels or wall faces in a plan, section, elevation or detail view.\n\nWhat this handles reliably, and what it does not - the Revit API is genuinely\nuneven here, so be aware before relying on it:\n\n  - grids and levels: resolved through their geometry in the target view with\n    references computed. This only works in a view where the datum is\n    actually visible; if it is not, the call says so by name rather than\n    throwing.\n  - wall faces: resolved with HostObjectUtils.GetSideFaces, choosing the\n    exterior or interior face. face='centerline' dimensions to the wall\n    element itself. Curtain walls and stacked walls often expose no side\n    face, and that is reported per reference.\n  - any other element: an element-level reference, which Revit accepts for\n    some categories and rejects for others.\n  - NOT handled: dimensioning in a 3D view (Revit forbids it, and the call\n    refuses up front), family instance sub-references such as a door's\n    centreline or a column's individual faces, and radial, angular, arc-length\n    or ordinate dimensions.\n\nRevit also requires the references to be parallel to each other and the\ndimension line parallel to them. When it refuses, the error says so and names\nthe usual causes; passing an explicit `line` is the reliable fix.\n\nIf `line` is omitted it is derived from the first and last reference points,\noffset perpendicular by `offset`. That is convenient but explicit is safer.\n\nReferences that cannot be resolved are listed in `unresolved` rather than\nfailing the call, as long as at least two resolve.\n\nReturns:\n    str: JSON with {dimension_id, view, reference_count, references,\n         unresolved, dimension_line: {source, start, end}, dimension_type,\n         segment_count, value_feet, value_string, units}"
    },
    {
      "name": "revit_create_floor",
      "description": "Create a floor from a list of boundary points on a specified level.\n\nArgs:\n    params.level_id: Target level ElementId\n    params.points: List of {x, y} boundary points (minimum 3, in feet, defines a closed polygon)\n\nReturns:\n    str: JSON with {element_id, type: 'Floor'}"
    },
    {
      "name": "revit_create_grid",
      "description": "Create straight grid lines from start/end points, in one undo step.\n\nGrids are vertical planes, so only x and y matter; any z is ignored and the\nresponse says so. Coordinates are in `units` and are echoed back in feet.\n\nArgs:\n    params.units: unit the coordinates are in (required)\n    params.grids: [{name, start:{x,y}, end:{x,y}}, ...]\n    params.dry_run: preview only\n\nReturns:\n    str: JSON with {grids: [{element_id, name}], plan, conflicts, undo}"
    },
    {
      "name": "revit_create_level",
      "description": "Create one or many levels at given elevations, in one undo step.\n\nElevations are in `units` and the response echoes both the supplied value and\nthe applied value in feet. A dry run lists the existing levels alongside the\nplan and flags name clashes (Revit requires unique level names) and\nduplicate elevations before anything is created.\n\nArgs:\n    params.units: unit the elevations are in (required)\n    params.levels: [{name, elevation}, ...]\n    params.dry_run: preview only\n\nReturns:\n    str: JSON with {levels: [{element_id, name, elevation_supplied, units,\n         elevation_ft}], conflicts, undo}"
    },
    {
      "name": "revit_create_revision",
      "description": "Create a revision and assign it to sheets and/or existing revision clouds.\n\nSheets keep their existing revisions - the new one is added alongside. If\nissued=true the revision is marked issued only after every assignment is\ndone, because Revit locks an issued revision against further edits.\n\nThis does not draw clouds; pass the ids of clouds that already exist.\n\nReturns:\n    str: JSON with {revision_id, revision_number, sequence_number,\n         description, date, issued_by, issued_to, issued, visibility,\n         sheets_assigned, sheets, revision_clouds}"
    },
    {
      "name": "revit_create_room",
      "description": "Create a room on a specified level at a seed point location.\n\nThe room will automatically find its boundary from surrounding walls.\n\nArgs:\n    params.level_id: Target level ElementId\n    params.x, y: Seed point inside the room boundary (in feet)\n    params.name: Room name\n\nReturns:\n    str: JSON with {element_id, name, number}"
    },
    {
      "name": "revit_create_sheet",
      "description": "Create one sheet from a titleblock type, with a number and name. The sheet number is checked for uniqueness first.\n\nUse revit_batch_create_sheets for a whole sheet set - it validates the entire\nlist before creating anything and commits as one undo step.\n\nReturns:\n    str: JSON with {sheet_id, sheet_number, sheet_name, is_placeholder,\n         titleblock_type, titleblock_type_id,\n         parameters: [{parameter, value, ok, error}]}"
    },
    {
      "name": "revit_create_text_note",
      "description": "Place a text note at a point in a view or on a sheet, with a given text style, width and alignment.\n\n`width` is the paper width the text wraps at. Revit only accepts a width\ninside a range that depends on the text type, so the width is checked first\nand a bad one comes back naming the allowed range instead of as an opaque\nRevit exception. Omit width for a note that does not wrap.\n\nThe position is in the target's own coordinates: model coordinates in a\nmodel view, sheet coordinates on a sheet. The response echoes both the\nrequested and the actual position.\n\nReturns:\n    str: JSON with {text_note_id, view, text_type, text_type_id,\n         requested_position, actual_position, width, width_unbounded, units,\n         character_count}"
    },
    {
      "name": "revit_create_view",
      "description": "Create a floor plan, ceiling plan, structural plan, area plan, section, elevation, detail, 3D, drafting or legend view.\n\nWhat each kind needs beyond name and view_kind:\n  - plans: level_id or level_name\n  - section / detail: start and end of the cut line in plan, plus optional\n    base_elevation, top_elevation and depth. Looking from start to end the\n    view looks to the right of travel, matching the Revit UI.\n  - elevation: origin (where the marker goes), optional host_plan_view_id\n    and elevation_index 0-3\n  - 3d, drafting: nothing extra\n  - legend: the Revit API cannot author a legend from nothing. This\n    duplicates an existing legend; pass source_view_id to choose which. If\n    the project has no legend at all the call says exactly that.\n\nThe name is checked against existing views in the same view family, which is\nthe scope Revit enforces - a floor plan and an area plan may share a name.\n\nReturns:\n    str: JSON with {view: {id, name, view_type, scale, is_template,\n         view_template_id}, view_kind, elevation_marker_id, units, note}"
    },
    {
      "name": "revit_create_view_filter",
      "description": "Create a rule-based view filter, add it to views, and set its graphic overrides.\n\nParameters can be named the way they appear in Revit's filter dialog. The\nbridge resolves the name against the parameters Revit reports as actually\nfilterable for the chosen categories, and when the name does not match it\nreturns the list of ones that do, so the next call can be right.\n\nString comparisons are case-insensitive: Revit 2027 removed the\ncase-sensitive rule overloads, so only the form that exists on every\nsupported version is used.\n\nA filter with no rules matches every element of the categories, which is\nlegitimate for \"colour all walls\"; the response flags that as\nmatches_everything_in_categories so it is never a silent surprise.\n\nReturns:\n    str: JSON with {filter_id, filter_name, replaced_existing, categories,\n         match, rules, rule_count, matches_everything_in_categories,\n         overrides_applied, filter_visible, filter_enabled, applied_to,\n         failed, failures}"
    },
    {
      "name": "revit_create_wall",
      "description": "Create a straight wall between two points on a specified level.\n\nCoordinates are in feet (Revit's internal unit). Uses the default wall type.\n\nArgs:\n    params.x1, y1: Start point\n    params.x2, y2: End point\n    params.level_id: Target level ElementId (use revit_get_levels to find IDs)\n\nReturns:\n    str: JSON with {element_id, type: 'Wall'}"
    },
    {
      "name": "revit_delete_element",
      "description": "Delete a Revit element by its ElementId. This action cannot be undone via MCP — use with caution.\n\nArgs:\n    params.element_id: ElementId of the element to delete\n\nReturns:\n    str: JSON with {deleted_element_id}"
    },
    {
      "name": "revit_delete_elements",
      "description": "Delete many elements at once. DESTRUCTIVE - dry_run defaults to True.\n\nDeleting in Revit pulls dependents along: a wall takes its doors and windows\nwith it. This reports the full picture before anything goes - the selected\nelements grouped by category, and separately everything Revit's dependency\ngraph says will follow.\n\nTwo guardrails, both deliberate:\n  - pinned elements are skipped unless include_pinned=True\n  - the operation refuses if the total exceeds max_delete (default 1000)\n\nEverything deleted goes in one TransactionGroup, so a single Ctrl+Z in Revit\nbrings it all back.\n\nArgs:\n    params.element_ids / params.filter: what to delete\n    params.dry_run: True by default - set False to actually delete\n    params.include_pinned: override the pin protection\n    params.max_delete: raise the ceiling deliberately\n\nReturns:\n    str: JSON with {plan{direct_by_category, dependent_by_category,\n         pinned_protected, total_affected}, deleted_count, deleted_ids, undo}"
    },
    {
      "name": "revit_delete_empty_tags",
      "description": "DESTRUCTIVE. Delete tags that are orphaned, host-less or render as blank. Dry run by default.\n\ndry_run defaults to TRUE. Each candidate is listed with the reason it\nqualified: orphaned (the element it tagged is gone), no host (it references\nno live element), no visible content (it renders as empty text), or - for\nroom tags - the room is gone or the tag head sits outside its room.\n\nSet include_empty_text=false to delete only genuinely orphaned and host-less\ntags, leaving blank-but-attached ones alone.\n\nShow the user the dry-run list before re-running with dry_run=false.\n\nReturns:\n    str: JSON with {dry_run, destructive, views_scanned, would_delete|deleted,\n         failed, failures, tags: [{tag_id, tag_kind, category, view_id,\n         view_name, tag_text, reason}]}"
    },
    {
      "name": "revit_delete_views",
      "description": "DESTRUCTIVE. Delete views. Dry run by default, and warns which views are placed on sheets.\n\ndry_run defaults to TRUE. The dry run lists every selected view with the\nsheet it sits on (if any) and its dependent-view count. Views on a sheet are\nblocked unless allow_placed_on_sheet=true, because deleting one deletes its\nviewport and leaves a gap in the drawing set. Sheets, the active view and\nRevit's browser pseudo-views are always refused.\n\nShow the user the dry-run list before re-running with dry_run=false.\n\nReturns:\n    str: JSON with {dry_run, destructive, would_delete|deleted, blocked,\n         warnings, views: [{view_id, name, view_type, is_template, on_sheet,\n         dependent_view_count, blocked}]}"
    },
    {
      "name": "revit_duplicate_view",
      "description": "Duplicate a view, optionally with detailing or as a dependent, and optionally rename the copy.\n\nRevit is asked whether the view supports the chosen option before anything is\ncreated, so an unsupported combination returns a clear message rather than a\nRevit exception.\n\nReturns:\n    str: JSON with {source_view, duplicate, duplicate_option, note}"
    },
    {
      "name": "revit_export_all_schedules_csv",
      "description": "Export all schedules in the Revit model as CSV files to a local folder.\n\nArgs:\n    params.output_dir: Folder path to write CSV files (e.g. 'C:/Exports/Schedules')\n\nReturns:\n    str: JSON with {exported_schedules: [{schedule, file}, ...], output_dir}"
    },
    {
      "name": "revit_export_dwg",
      "description": "Export views or sheets to DWG, one file per sheet by default.\n\nTIMING: each sheet is a separate export, so this works to a time budget and\nreturns EARLY rather than being cut off at 60s - the response carries the files\nactually written, `completed`, and `remaining_views` to pass back as view_ids on\nthe next call. Expect roughly a handful of sheets per call on a real model.\nSetting single_file_per_sheet=false merges everything into one DWG, which is a\nsingle un-chunkable call and can time out.\n\nName the scope with view_set_name (a saved set), sheet_numbers, view_names or\nview_ids. Prefer a saved setup_name over restating options - see\nrevit_get_export_setups.\n\nArgs:\n    params.output_dir: folder to write into\n    params.view_set_name / sheet_numbers / view_names / view_ids: what to export\n    params.setup_name: a DWG export setup saved in the model\n    params.layer_mapping: AIA, BS1192, CP83, DIN, ISO13567 or GSA\n    params.single_file_per_sheet: one DWG per sheet (default true)\n\nReturns:\n    str: JSON with {exported: [{view, files}], completed, remaining_views,\n         file_count, total_bytes, failures, elapsed_seconds}"
    },
    {
      "name": "revit_export_elements_csv",
      "description": "Export elements (optionally scoped by category and/or element ID) plus chosen\nparameters to a single CSV on disk. Always includes ElementId, Category,\nFamilyAndType, Workset, and Level columns, plus one column per name in\nproperty_names (e.g. 'WORK PACKAGE', '(PFB) PACKAGE NAME').\n\nThis is the bulk equivalent of calling revit_get_element_by_id one element at a\ntime -- use this instead of a loop of individual lookups when you need parameter\nvalues across many elements (e.g. building a work-package completion tracker).\n\nArgs:\n    params.output_path: Full file path to write the CSV to\n    params.property_names: Parameter names to include as extra columns\n    params.categories: Optional list of category names to scope the export\n    params.element_ids: Optional list of specific ElementIds to scope the export\n\nReturns:\n    str: JSON with {output_path, element_count, categories, columns}"
    },
    {
      "name": "revit_export_gbxml",
      "description": "Export the model to gbXML for energy analysis.\n\nTIMING: whole-model and un-chunkable. Revit rebuilds the energy analytical model\nfirst, which on a large model regularly exceeds the 60s bridge timeout. Run\nestimate_only=true first.\n\ngbXML is only meaningful if the model has rooms or MEP spaces - the response\nreports those counts and warns plainly when there are none, because Revit will\notherwise write a file that carries no thermal zones and is useless downstream.\n\nArgs:\n    params.output_dir: folder to write into\n    params.file_name: output name without extension\n    params.export_analytical_systems: ignored on Revit 2026+, which always\n        exports them; the response says which happened\n\nReturns:\n    str: JSON with {files, room_count, space_count, readiness, analytical_systems,\n         elapsed_seconds}"
    },
    {
      "name": "revit_export_history",
      "description": "Read back the export manifest: what was exported when, against which model, with what totals.\n\nAnswers \"how has this quantity moved over the last month\" without reopening\nany old model, because each row carries the totals as they stood at export\ntime. The `series` field comes back oldest-first and is the one to plot or\ntabulate; `exports` carries the full manifest rows.\n\nRows whose exported file has since been deleted are still returned, with\nfile_exists=false, so a gap in the trend is visible rather than silent.\n\nArgs:\n    params.project / params.all_projects: Whose manifest to read\n    params.kind / params.label / params.since: Filters\n    params.limit: Max rows, newest first (default 50)\n\nReturns:\n    str: JSON with {root, projects, manifests, total_exports, returned,\n         exports: [{timestamp_utc, export_id, label, kind, scope, project_name,\n         project_number, document_path, model_guid, revit_version, file_path,\n         row_count, snapshot_id, metrics, file_exists}], series}"
    },
    {
      "name": "revit_export_ifc",
      "description": "Export the Revit model to IFC, with control over the schema/MVD, the export\nview, space boundaries, wall splitting and the saved mapping templates.\n\nTIMING: IFC export is ONE un-chunkable operation and the bridge gives up after\n60 seconds. On anything bigger than a sample model a whole-model IFC export\nwill very likely exceed that. Scope it to a single 3D view (view_name), or run\nestimate_only=true first to see the element count and the warning. If it does\ntime out, nothing partial is reported as success - but Revit may still finish\nwriting the file in the background, so check the folder before retrying.\n\nVERSION-GATED OPTIONS:\n  category_template  - controls which categories export and to which IFC\n                       entity. Revit 2025+ only; refused with an explanation\n                       on 2024 rather than silently ignored.\n  parameter_template - controls property-set mapping. Revit 2026+ only.\nBoth are saved templates in the model; list them with revit_get_export_setups.\nThey are activated for the export and then restored, so the model is left as\nit was found.\n\nArgs:\n    params.output_dir: folder to write into\n    params.ifc_version: e.g. 'IFC2x3 Coordination View 2.0', 'IFC4 Reference View'\n    params.view_name: export only this view - the best way to bound the runtime\n    params.space_boundaries: 0 none, 1 first level, 2 second level\n    params.split_walls_by_level: split multi-storey walls/columns by level\n    params.category_template / params.parameter_template: saved mapping templates\n\nReturns:\n    str: JSON with {files: [{file, bytes, size}], ifc_version, scoped_to_view,\n         category_template_applied, parameter_template_applied, elapsed_seconds}"
    },
    {
      "name": "revit_export_image",
      "description": "Export views as raster images (PNG, JPEG, TIFF, BMP, TARGA).\n\nEach view is rendered separately, so filenames are predictable, the reported\nsizes are real, and a long run returns early with `remaining_views` instead of\nhitting the 60s bridge timeout. Rendered/shaded 3D views are much slower than\nline drawings - a handful of those can fill the budget on their own.\n\nexport_range=CurrentView or VisibleRegionOfCurrentView captures whatever view is\nactive in Revit at that moment, which may not be the one you have in mind; use\nSetOfViews with an explicit scope when it matters.\n\nArgs:\n    params.output_dir: folder to write into\n    params.export_range: SetOfViews (default), CurrentView, VisibleRegionOfCurrentView\n    params.view_names / view_ids / sheet_numbers / view_set_name: which views\n    params.pixel_size: long-edge pixels when zoom_type=FitToPage (default 2048)\n    params.resolution: 72, 150 (default), 300 or 600 DPI\n    params.file_type: PNG (default), TIFF, JPEGMedium, ...\n\nReturns:\n    str: JSON with {exported: [{view, files}], completed, remaining_views,\n         file_count, total_bytes, elapsed_seconds}"
    },
    {
      "name": "revit_export_nwc",
      "description": "Export a Navisworks cache (NWC) file - IF the Navisworks exporter add-in is\ninstalled for the connected Revit.\n\nREAD THIS BEFORE USING: NWC export is NOT part of the Revit API. It is provided\nby the separate free 'Autodesk Navisworks Exporters' add-in, and the Revit API\ntypes are only a shim onto it. If that add-in is not registered for the running\nRevit version there is no fallback and no workaround, and this tool returns a\nspecific error saying so rather than failing obscurely. Installing Navisworks\nitself does NOT install the Revit-side exporter.\n\nCall with probe_only=true first: it reports whether the exporter is available,\nwhich Navisworks products are on the machine, and exactly which folders were\nsearched - without exporting anything.\n\nTIMING: a whole-model NWC is one un-chunkable operation and regularly runs past\nthe 60s bridge timeout on a coordination model. Scope it to a single 3D view\n(view_name) where you can.\n\nArgs:\n    params.probe_only: report exporter availability only\n    params.output_dir: folder to write into\n    params.view_name: export only this view\n    params.export_scope: Model, View or SelectedElements\n    params.coordinates: Internal or Shared\n    params.divide_file_into_levels / export_links / export_parts / ...\n\nReturns:\n    str: JSON with {exporter: {exporter_installed, addin_manifests_found,\n         navisworks_products_on_machine, locations_searched}, files, elapsed_seconds}"
    },
    {
      "name": "revit_export_pdf",
      "description": "Export a set of sheets to PDF - combined into one file, or one PDF per sheet.\n\nTIMING: combine=true (the default) is a SINGLE un-chunkable Revit call; a large\nsheet set can exceed the 60s bridge timeout. combine=false exports sheet by\nsheet, works to a time budget, and returns early with `completed` and\n`remaining_views` so the run can be continued. For a big issue, prefer\ncombine=false, or scope with sheet_numbers.\n\nBackground PDF export is forced off where Revit exposes the switch (2025+),\nbecause a background export hands back control before the files exist and there\nwould be nothing truthful to report.\n\nArgs:\n    params.output_dir: folder to write into\n    params.view_set_name / sheet_numbers / all_sheets: which sheets\n    params.combine: one combined PDF (default) or one per sheet\n    params.naming_rule: per-sheet filename rule (combine=false only)\n    params.paper_format / paper_orientation: e.g. 'ISO_A1', 'Landscape'\n    params.hide_crop_boundaries / hide_scope_boxes / hide_unreferenced_view_tags\n\nReturns:\n    str: JSON with {files or exported, completed, remaining_views, file_count,\n         total_bytes, naming_rule, elapsed_seconds}"
    },
    {
      "name": "revit_export_schedules_xlsx",
      "description": "Export Revit schedules to a real multi-sheet Excel workbook - one worksheet\nper schedule, keeping each schedule's own column headings and its grouping.\n\nThis is the richer counterpart to revit_export_all_schedules_csv: CSV flattens\na schedule into one file each and loses the grouping entirely. Here the group\nheader rows Revit draws are preserved as merged, bold, shaded rows above the\ndata they group, the header row is frozen, plain numeric cells are written as\nreal numbers so Excel can total them, and an index worksheet lists every\nschedule with its row counts.\n\nTIMING: schedules are cheap compared with geometry exports, but a model with\nmany large schedules can still exceed the 60s bridge timeout. The bridge works\nto a time budget and reports any schedules it did not reach; name a subset with\nschedule_names to fetch the rest and they can be merged in a second pass.\n\nArgs:\n    params.output_path: full path of the .xlsx to write\n    params.schedule_names: subset of schedules (omit for all)\n    params.overwrite: replace an existing workbook\n    params.max_rows_per_schedule: row cap per schedule (default 5000)\n    params.coerce_numbers: write plain numbers as numbers (default true)\n\nReturns:\n    str: JSON with {output_path, bytes, worksheets: [{schedule, worksheet, rows,\n         group_rows, truncated}], schedules_written, not_reached, elapsed_seconds}"
    },
    {
      "name": "revit_export_timestamped",
      "description": "Run a data export to a dated path and record it in a manifest, so quantities can be trended over time.\n\nUnlike revit_export_elements_csv, which writes wherever you point it and\nleaves no record, this writes to\n%LOCALAPPDATA%\\CampbellCTS\\RevitMcp\\exports\\<project>\\<date>\\ and appends a\nrow to that project's manifest.csv capturing: UTC timestamp, project name and\nnumber, document path, model GUID, Revit version, whether the model is\nworkshared, the central path, the file written, the row count, and the id of\nany snapshot taken in the same call.\n\nThat manifest is the point: run this weekly and revit_export_history can trend\na quantity across weeks, and you can prove what the numbers were on a given day.\nPass with_snapshot=True to tie each export to the exact model state it came from.\n\nArgs:\n    params.kind: 'elements', 'schedules' or 'quantities'\n    params.label: Short label for the manifest row and filename\n    params.categories / params.property_names / params.element_ids: kind='elements' scope\n    params.category / params.limit: kind='quantities' scope\n    params.with_snapshot: Also capture a snapshot and record its id on the row\n\nReturns:\n    str: JSON with {export_id, kind, label, project, timestamp_utc, output,\n         row_count, column_count, manifest_path, snapshot, metrics, detail, model}"
    },
    {
      "name": "revit_export_view_set",
      "description": "Export a saved view/sheet set to PDF, DWG or images in one call.\n\nThis is how a drawing issue actually works in an office: the set is the\ndeliverable, and the request is \"issue the S-Series set as PDF\". Use\nrevit_get_export_setups to see the sets saved in the model.\n\nTIMING: the same budgets apply as for the individual export tools - a large set\nto per-sheet PDF or DWG returns early with `completed` and `remaining_views`;\na combined PDF is one un-chunkable call.\n\nIFC, NWC and gbXML are whole-model exports and are refused here with an\nexplanation - use revit_export_ifc / revit_export_nwc / revit_export_gbxml with a\nsingle 3D view instead.\n\nArgs:\n    params.view_set_name: the saved set to issue\n    params.format: pdf (default), dwg or image\n    params.output_dir: folder to write into\n    params.sheets_only: ignore non-sheet views in the set\n    params.setup_name: a saved PDF or DWG setup to use\n\nReturns:\n    str: JSON in the same shape as the matching per-format tool."
    },
    {
      "name": "revit_export_warnings_report",
      "description": "Export every model warning to CSV, grouped by type, with the elements involved.\n\nWrites one row per element per warning, including the element's category, type,\nlevel, workset and design option. The model is not modified; only the CSV file\nis written.\n\nReturns:\n    str: JSON with {output_path, rows_written, total_warnings,\n                    distinct_warning_types, error_severity_count,\n                    by_type: [{warning_type, guid, severity, count, distinct_elements}]}"
    },
    {
      "name": "revit_find_duplicate_elements",
      "description": "Find coincident duplicated instances: same type, same place within a tolerance.\n\nPoint-located elements are compared on their insertion point; line-located\nelements must match at both ends (either way round), so two different-length\nbeams that merely share a midpoint are not reported.\n\nRotation is not compared, and elements in different phases or design options are\nnot distinguished - check the reported ids before deleting anything.\n\nReturns:\n    str: JSON with {tolerance_ft, tolerance_mm, elements_scanned, total_groups,\n                    elements_in_groups, returned, truncated,\n                    duplicate_groups: [{category, type_name, count, element_ids,\n                                        location, level}], limitations}"
    },
    {
      "name": "revit_find_elements_missing_param",
      "description": "Find elements whose value for a given parameter is genuinely missing.\n\n\"Missing\" is decided per parameter storage type: empty or whitespace text, an\nunset ElementId reference, or a parameter that has never been given a value.\nA numeric 0 is a real value and is not reported unless zero_counts_as_missing\nis set.\n\nThe response reports the true total found and whether the returned page was\ntruncated, so a result can be trusted and paged through.\n\nReturns:\n    str: JSON with {parameter, scanned_elements, elements_with_parameter,\n                    elements_with_value, total_missing, returned, offset, limit,\n                    truncated, missing_by_reason, missing_by_category,\n                    elements_missing: [{id, name, category, type_name,\n                                        storage_type, reason, current_value}],\n                    elapsed_ms, hint, note}"
    },
    {
      "name": "revit_find_replace_text",
      "description": "Find and replace across text notes, and optionally inside a named text parameter on elements. Dry run by default.\n\ndry_run defaults to TRUE and the dry run shows every match with its exact\nbefore and after text and where it lives, so the change can be reviewed\nbefore anything is written. Show the user that list before re-running with\ndry_run=false.\n\nOnly text (string) parameters are searched; read-only ones are flagged in the\ndry run and reported as failures rather than silently skipped. An invalid\nregular expression is rejected up front with the parse error.\n\nAll the replacements commit as one undo step.\n\nReturns:\n    str: JSON with {dry_run, find, replace, regex, match_case, scope,\n         match_count, replaced, failed, failures, matches: [{element_id, kind,\n         parameter, where, before, after, read_only}]}"
    },
    {
      "name": "revit_find_unplaced_rooms",
      "description": "Find all rooms that are unplaced or have zero area (not properly bounded).\n\nThe result is capped and reports the true total, so a large model returns a\nfirst page rather than everything at once.\n\nReturns:\n    str: JSON with {total, returned, offset, limit, truncated,\n                    rooms: [{id, name, number, area}]}"
    },
    {
      "name": "revit_find_untagged_elements",
      "description": "QA pass: list every element of the given categories that has no tag, in one view or across all taggable views.\n\nRead-only - it changes nothing. Sheets, schedules and view templates are not\naudited because tags cannot live in them.\n\nThe response carries a per-view summary (how many elements of each category\nare in the view and how many are untagged) plus a flat, paged list of the\nuntagged elements themselves, so a large model stays readable.\n\nReturns:\n    str: JSON with {scope, categories, elements_checked, untagged_total,\n         views_summary: [{view_id, view_name, view_type, elements_in_view,\n         untagged, by_category}], total_rows, offset, limit,\n         untagged_elements: [{view_id, view_name, category, element_id,\n         element_name, type_name}]}"
    },
    {
      "name": "revit_get_active_view",
      "description": "Get the active Revit view: name, id, type, scale, discipline, detail level, crop state, whether it is a sheet.\n\nCall this before capturing an image or changing view graphics, so you know\nwhat the user is looking at and whether the view even allows overrides.\n\nReturns:\n    str: JSON with {document, active_view: {id, name, view_type, is_sheet,\n         sheet_number, is_template, scale, discipline, detail_level,\n         crop_box_active, crop_box_visible, view_template, can_be_printed,\n         graphics_overrides_allowed, supports_temporary_visibility,\n         temporary_hide_isolate_active}, active_graphical_view_id,\n         selected_count, open_view_ids}"
    },
    {
      "name": "revit_get_all_elements",
      "description": "Get a paginated list of all model elements with basic info.\n\nUse revit_get_elements_by_category for more targeted queries.\n\nArgs:\n    params: limit (default 100), offset (default 0)\n\nReturns:\n    str: JSON array of {id, name, category, type_id}"
    },
    {
      "name": "revit_get_categories",
      "description": "List every category present in this model with a live instance count.\n\nCall this before guessing a category name for revit_query_elements - category names\nare localised, several of them collide, and a model only contains a fraction of the\ncategories Revit supports. By default only categories with at least one placed\ninstance are listed, which is the honest answer to \"what is in this model\".\n\nEach row carries the BuiltInCategory name (e.g. OST_StructuralFraming), which is the\nunambiguous handle and is accepted anywhere a category name is.\n\nArgs:\n    params.include_empty: also list categories with zero instances (default false)\n    params.include_subcategories: also list subcategories (default false)\n    params.name_contains / params.category_type: narrow the list\n    params.limit: max 2000, default 300\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, categories:\n         [{id, name, builtin_category, category_type, is_model, is_annotation,\n         is_analytical, is_cuttable, allows_bound_parameters,\n         has_material_quantities, is_subcategory, parent_category, instance_count}]}\n         ordered by instance_count, busiest first."
    },
    {
      "name": "revit_get_clash_summary",
      "description": "Real clash detection between two sets of elements defined by category.\n\nA bounding-box pre-pass narrows the candidates, then an exact solid-solid\nintersection test confirms each one, then a boolean Intersect gives the\ninterpenetration volume and a centroid to zoom to.\n\nCost is O(n x m) bounding-box comparisons before anything is filtered, so\nscope the run with `level` or `workset`. Without a scope, a large pair count\nis refused with the numbers rather than run - pass allow_unscoped=True to\noverride. Joined and host/hosted pairs are skipped by default because those\nare modelling intent rather than clashes.\n\nOnly elements in the open document are compared; linked models are not\ntraversed, and clearance (near-miss) checking is not supported.\n\nReturns:\n    str: JSON with {refused, method, set_a_count, set_b_count, bbox_tests,\n                    candidate_pairs_after_bbox, total_clashes, returned, truncated,\n                    skipped_joined_pairs, skipped_hosted_pairs, elapsed_ms,\n                    clashes: [{a:{id,category,name,type_name,level},\n                               b:{...}, intersection_volume_ft3,\n                               intersection_volume_m3, centroid:{x,y,z}}],\n                    limitations}"
    },
    {
      "name": "revit_get_design_options",
      "description": "List design option sets, the options in each, which is primary, and element counts.\n\nElements inside a non-primary design option are invisible to most views and to any\nquery that does not ask for them, so an unexpected element count is often explained\nhere. The counts come from Revit's own ElementDesignOptionFilter.\n\nArgs:\n    params.limit: max 500, default 100\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated,\n         option_set_count, option_sets: [{id, name}], options:\n         [{id, name, is_primary, option_set_id, option_set, element_count}]}"
    },
    {
      "name": "revit_get_element_bbox",
      "description": "Bounding boxes for a list of elements, in project and shared coordinates.\n\nShared coordinates are reported wherever the model has a shared coordinate system,\nwhich is what you need to compare positions against a linked model or a survey.\nAll eight corners are transformed and re-bounded, so a rotated shared site gives a\ncorrect box rather than a misleading one.\n\nArgs:\n    params.element_ids: elements to measure\n    params.units: ft / in / mm / cm / m (default ft)\n    params.view_id: optionally report the box as cropped in that view\n    params.limit: max 500, default 100\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, units,\n         shared_coordinate_system, has_shared_coordinates, elements:\n         [{element_id, found, name, category, has_geometry,\n         project: {min, max, size, center}, shared: {min, max, size, center}}]}"
    },
    {
      "name": "revit_get_element_by_id",
      "description": "Get detailed information about a specific Revit element including all its parameters.\n\nArgs:\n    params.element_id: The Revit integer ElementId\n\nReturns:\n    str: JSON with {id, name, category, type_name, parameters: {name: value, ...}}"
    },
    {
      "name": "revit_get_element_geometry",
      "description": "Summarise an element's actual geometry: solids, volume, surface area, faces, curves.\n\nDeliberately a summary rather than a mesh dump - you get counts, totals, a breakdown\nof face kinds (PlanarFace, CylindricalFace, ...), and for line-based elements the\nreal centreline curve with its endpoints, length and radius. That is enough to\nreason about shape without flooding the context.\n\nGeometry extraction is the most expensive read in this toolset, so the per-call\nelement limit is low (max 50, default 10). Ask for the elements you actually need.\n\nArgs:\n    params.element_ids: elements to summarise\n    params.units: lengths in these units; areas and volumes in its square and cube\n    params.detail_level: coarse / medium / fine (default fine)\n    params.max_curves: cap on curves reported per element (default 20, max 200)\n    params.limit: max 50, default 10\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, units,\n         area_units, volume_units, max_curves, elements: [{element_id, name,\n         category, solid_count, total_volume, total_surface_area, face_count,\n         edge_count, face_kinds, instance_count, mesh_count, location_curve,\n         curves, curves_truncated, geometry_error}]}"
    },
    {
      "name": "revit_get_element_history",
      "description": "Get who created, who owns and who last changed specific elements, from Revit's worksharing data.\n\nONLY workshared (central-file) models carry this. On a model that is not\nworkshared the response comes back with workshared=false, available=false and\nan explanation -- report that, do not present it as \"no changes found\".\n\nTwo fields are meaningfully empty and come back as null rather than \"\":\nowner is null when nobody currently owns the element, and last_changed_by is\nnull when it has not been modified since it was created.\n\nRevit records no per-element edit timestamps, so this answers \"who\" but not\n\"when\". For \"what changed since Tuesday and who did it\", use\nrevit_get_recent_changes, which combines this with a snapshot diff.\n\nArgs:\n    params.element_ids: Specific ElementIds to look up, or\n    params.category: Every element of this category (capped by limit)\n    params.limit: Max elements to look up (default 200)\n\nReturns:\n    str: JSON with {workshared, available, document, central_path, requested,\n         returned, truncated, elements_not_found,\n         elements: [{id, name, category, type, workset, creator, owner,\n         last_changed_by}], notes}"
    },
    {
      "name": "revit_get_element_parameters",
      "description": "Every parameter on one element, with both the raw value and the display string.\n\nUse this to discover the exact parameter names and spellings to feed into\nrevit_query_elements, and to read a value properly. Unlike\nrevit_get_element_by_id, which returns raw numbers only, each parameter here\ncarries `value_string` - the same value formatted in the project's display units,\nwhich is what the user sees in Revit - plus whether it is read-only and, for a\nshared parameter, its GUID.\n\nInstance parameters come first; type parameters of the same name are shadowed by\nthe instance, matching how Revit itself resolves them.\n\nArgs:\n    params.element_id: the element to read\n    params.include_type_parameters: also include unshadowed type parameters (default true)\n    params.name_contains / params.with_value_only: narrow the list\n    params.limit: max 1000, default 200\n\nReturns:\n    str: JSON with {element_id, name, category, type_id, type_name, is_type,\n         total_count, returned, offset, limit, truncated, parameters:\n         [{name, source, storage_type, is_read_only, has_value, is_shared, guid,\n         builtin_parameter, group, spec, value, value_string}]}"
    },
    {
      "name": "revit_get_element_relationships",
      "description": "How elements connect to the rest of the model: host, hosted, group, assembly, MEP system, rooms.\n\nAnswers the questions a flat element list cannot: what wall is this door in, what\nis cut into this wall, is this element inside a group you must not edit in place,\nwhich MEP system does this fitting belong to, and - for a door or window - which\nrooms are on either side of it.\n\nArgs:\n    params.element_ids: elements to report on\n    params.limit: max 200, default 50\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, elements:\n         [{element_id, name, category, host, hosted_elements, hosted_truncated,\n         super_component, sub_components, group, assembly, design_option,\n         mep_systems, mep_system_name, from_room, to_room}]}"
    },
    {
      "name": "revit_get_element_room",
      "description": "Which room or MEP space each of a list of elements sits in.\n\nUses the element's own Room property where Revit tracks one (doors, windows, most\nplaced components) and otherwise falls back to a point-in-room test at the element's\nlocation - `resolved_by` tells you which was used, which matters because the\nfallback can be wrong for an element whose centre falls outside its room.\n\nArgs:\n    params.element_ids: elements to locate\n    params.phase: phase to resolve rooms in (defaults to the last project phase)\n    params.limit: max 500, default 100\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, units, phase,\n         elements: [{element_id, name, category, test_point,\n         room: {id, name, number, level}, space, resolved_by}]}"
    },
    {
      "name": "revit_get_elements_by_category",
      "description": "Get all elements of a specific Revit category (e.g. 'Walls', 'Doors', 'Structural Columns').\n\nArgs:\n    params.category: Category name as it appears in Revit\n    params.limit: Max elements to return (default 200)\n\nReturns:\n    str: JSON array of {id, name, category, type_id}"
    },
    {
      "name": "revit_get_elements_by_type",
      "description": "Get all elements of a specific family type (e.g. 'Generic - 200mm', 'W12X26').\n\nArgs:\n    params.type_name: Exact family type name as it appears in Revit\n\nReturns:\n    str: JSON array of {id, name, category, type_id}"
    },
    {
      "name": "revit_get_elements_by_workset",
      "description": "Get elements belonging to a specific workset, with real pagination.\n\nUse revit_get_worksets first to see exact workset names. Unlike\nrevit_get_elements_by_category (which has no offset and silently caps at\nits limit), this returns total_count alongside the page so you know\nwhether more results exist beyond limit+offset -- call again with a\nhigher offset to page through the rest.\n\nArgs:\n    params.workset_name: Exact workset name, e.g. \"03-UG CONDUITS\"\n    params.category: Optional category filter within the workset, e.g. \"Conduits\"\n    params.limit: Max elements to return this call (default 200, max 2000)\n    params.offset: Elements to skip, for paging past `limit` (default 0)\n\nReturns:\n    str: JSON with {workset_name, category, total_count, offset, limit,\n         elements: [{id, name, category, type_id}, ...]}"
    },
    {
      "name": "revit_get_export_setups",
      "description": "List the export setups and view/sheet sets saved in the Revit model.\n\nCall this before any DWG, PDF or IFC export. Picking a setup the office has\nalready standardised on by name is far more reliable than restating two dozen\noptions, and the saved view/sheet sets are how drawing issues are actually\norganised.\n\nReturns DWG/DXF, DGN and PDF setup names, the IFC category templates\n(Revit 2025+) and property-set templates (Revit 2026+), which IFC schemas the\nconnected Revit supports, and every saved view/sheet set with its view and\nsheet counts.\n\nArgs:\n    params.ifc_category_mapping_for: also dump this IFC category template's\n        category->entity mapping (empty string = the active template)\n    params.exporting_only: with the above, only categories that export\n\nReturns:\n    str: JSON with {dwg_setups, dgn_setups, pdf_setups, ifc_category_templates,\n         ifc_parameter_templates, supported_ifc_versions, view_sheet_sets, ...}\n         plus ifc_category_mapping when requested."
    },
    {
      "name": "revit_get_families",
      "description": "List all families loaded in the Revit project.\n\nReturns:\n    str: JSON array of {id, name, category, is_in_place, type_count}"
    },
    {
      "name": "revit_get_grids",
      "description": "List the model's grid lines with their names, endpoints and 3D extents.\n\nGrids are how a building is described in conversation (\"the column at B/3\"), so this\nis what lets you translate between a grid reference and a coordinate.\n\nArgs:\n    params.units: units for coordinates and lengths (default ft)\n    params.limit: max 1000, default 200\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, units, grids:\n         [{id, name, is_curved, curve: {type, start, end, length, radius},\n         extents: {min, max}, type_name}]}"
    },
    {
      "name": "revit_get_levels",
      "description": "List all levels in the active Revit model with their elevations.\n\nReturns:\n    str: JSON array of {id, name, elevation, elevation_ft}"
    },
    {
      "name": "revit_get_links",
      "description": "Every RVT, IFC, CAD and point-cloud link or import, with its load state and path.\n\nCheck this before trusting any count. A link whose load_state is Unloaded or NotFound\ncontributes nothing to any other query in this toolset, so a total that should\ninclude linked elements will be short and nothing else will say so.\n\nArgs:\n    params.limit: max 500, default 100\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, links:\n         [{id, name, kind, is_import, load_state, reference_type, path_type, path,\n         instance_count}]} where kind is RVT, IFC, CAD or PointCloud."
    },
    {
      "name": "revit_get_materials",
      "description": "List materials with their identity data and their appearance / physical / thermal assets.\n\nArgs:\n    params.name_contains: only materials whose name contains this\n    params.limit: max 1000, default 100\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, materials:\n         [{id, name, material_class, material_category, color, transparency,\n         shininess, smoothness, use_render_appearance_for_shading,\n         appearance_asset, physical_asset, thermal_asset,\n         identity: {description, manufacturer, model, url, mark, keynote}}]}"
    },
    {
      "name": "revit_get_phases",
      "description": "Project phases in order, plus every phase filter and what it does.\n\n'sequence' 0 is the earliest phase. Each phase filter's `shows` maps an element's\nphase status (New, Existing, Demolished, Temporary, Past, Future) to how that status\nis drawn in a view using the filter - ByCategory, Overridden or NotDisplayed. That is\nwhat explains why an element is missing from a view even though it exists.\n\nArgs:\n    params.include_element_counts: also count elements created in each phase.\n        Costs one pass over the model, so it is off by default.\n    params.limit: max 500, default 100\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated,\n         phases: [{id, name, sequence, elements_created}],\n         phase_filters: [{id, name, shows: {status: presentation}}]}"
    },
    {
      "name": "revit_get_project_info",
      "description": "Get Revit project metadata including name, number, client, author, file path, and version.\n\nReturns:\n    str: JSON with keys: project_name, project_number, project_address,\n         client_name, author, building_name, organization, file_path,\n         is_workshared, revit_version"
    },
    {
      "name": "revit_get_project_location",
      "description": "Survey point, project base point, internal origin, true north and shared coordinates.\n\nThis is what relates model coordinates to the real world and to linked models.\nSurvey point coordinates are shared coordinates; project base point coordinates are\nproject coordinates; the internal origin is project (0,0,0) by definition, and its\nshared position is reported so you can convert between the two.\n'true_north_angle_degrees' is measured counter-clockwise from project north.\n\nArgs:\n    params.units: units for coordinates and elevations (default ft)\n\nReturns:\n    str: JSON with {units, shared_coordinate_system, has_shared_coordinates,\n         true_north_angle_degrees, project_position, survey_point,\n         project_base_point, internal_origin, site_location, project_locations}"
    },
    {
      "name": "revit_get_quantities",
      "description": "Get area, volume, and length quantities for elements of a specific category.\n\nArgs:\n    params.category: Revit category to quantify (e.g. 'Walls', 'Floors', 'Ceilings')\n    params.limit: Max element rows to return (default 500)\n    params.summary_only: If True, return only counts/totals with no per-element rows\n\nReturns:\n    str: JSON with {category, element_count, total_area, total_volume,\n         returned, truncated, elements: [...]}"
    },
    {
      "name": "revit_get_recent_changes",
      "description": "Answer \"what changed since <date>, and who did it\" in one call.\n\nRuns a snapshot diff and attaches Revit's worksharing Creator / Owner /\nLastChangedBy to every changed element that still exists in the model.\nRequires at least two snapshots for the project -- run revit_snapshot_create\nregularly (or with each revit_export_timestamped) so there is a baseline to\ncompare against.\n\nTwo honest limits, both restated in the response's `notes`:\nattribution is read from the CURRENT model, not from the snapshots, and\nremoved elements no longer exist so nothing can be attributed to them; and a\nmodel that is not workshared carries no attribution at all, so the \"what\nchanged\" half is complete while the \"who\" half does not exist.\n\nArgs:\n    params.since: Baseline is the newest snapshot at or before this UTC date/time\n    params.from_snapshot / params.to_snapshot: Explicit snapshots instead of `since`\n    params.project: Project to inspect, if not the open model\n    params.categories: Restrict to these categories\n    params.limit: Max changed elements returned (modified first, then added, then removed)\n    params.include_worksharing: Look up creator/owner/last_changed_by\n\nReturns:\n    str: JSON with {project, workshared, from, to, summary: {added, removed,\n         modified, unchanged}, by_category, warning_count, element_count,\n         parameter_detail_available, total_changes, truncated,\n         changes: [{id, change, category, type, level, workset, moved,\n         changes: [{parameter, from, to}], still_in_model, creator, owner,\n         last_changed_by, worksharing_available}], notes}"
    },
    {
      "name": "revit_get_schedule_data",
      "description": "Retrieve data rows from a named Revit schedule (quantity takeoff, door schedule, etc.).\n\nArgs:\n    params.schedule_name: Exact name of the schedule view in Revit\n    params.limit: Max rows to return (default 500)\n\nReturns:\n    str: JSON with {schedule_name, headers: [...], rows: [[...], ...], total_rows}"
    },
    {
      "name": "revit_get_selection",
      "description": "Get what the user currently has selected in Revit.\n\nStart here when the user says \"this wall\", \"these\", or \"what I've got\nselected\" - it is the only way to find out what they are pointing at.\n\nArgs:\n    params.limit: Max selected elements to return (default 200)\n    params.offset: Selected elements to skip, for paging\n    params.detailed: Include every parameter of each element\n\nReturns:\n    str: JSON with {view, total_count, offset, limit, returned, truncated,\n         element_ids, elements: [{id, name, category, type_id}, ...]}"
    },
    {
      "name": "revit_get_sheet_info",
      "description": "Full picture of one sheet: its viewports and the views they hold, its titleblock, its revisions and all its parameters.\n\nRead-only. Use this before placing or rearranging views on a sheet - it\nreports each viewport's real box centre and size, so a layout can work around\nwhat is already there.\n\nReturns:\n    str: JSON with {sheet_id, SheetNumber, sheet_name, is_placeholder,\n         sheet_collection_id, titleblock: {instance_id, type_id, family,\n         type_name, sheet_width, sheet_height}, viewport_count,\n         viewports: [{viewport_id, view_id, view_name, view_type, view_scale,\n         box_center, box_min, box_max, box_width, box_height, rotation}],\n         revision_count, revisions, revision_cloud_count, sheet_parameters,\n         titleblock_parameters, units}"
    },
    {
      "name": "revit_get_sheets",
      "description": "List all drawing sheets in the Revit model.\n\nReturns:\n    str: JSON array of {id, sheet_number, sheet_name, drawn_by}"
    },
    {
      "name": "revit_get_titleblocks",
      "description": "Available titleblock family types, their sheet size, and their parameters.\n\n`sheets_using` tells you which titleblock the project actually standardises on, which\nis usually the one you want when placing or auditing sheets.\n\nArgs:\n    params.include_parameters: include each type's parameters (default true)\n    params.units: units for sheet width and height (default ft)\n    params.limit: max 500, default 50\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, units,\n         titleblocks: [{id, family, type_name, sheet_width, sheet_height,\n         sheets_using, parameters}]}"
    },
    {
      "name": "revit_get_type_parameters",
      "description": "Every parameter on an element TYPE, in the same shape as revit_get_element_parameters.\n\nPass type_id directly, or pass element_id and the element's own type is resolved for\nyou - handy when you have an instance and want to know what its type controls.\n\nArgs:\n    params.type_id: the element type to read, OR\n    params.element_id: an instance whose type should be read\n    params.limit: max 1000, default 200\n\nReturns:\n    str: JSON in the same shape as revit_get_element_parameters, with every\n         parameter's source reported as \"type\"."
    },
    {
      "name": "revit_get_units",
      "description": "The project's display units per discipline - so you stop guessing what \"200\" means.\n\nIn a metric project 200 is 200 mm; in an imperial one it is 200 feet, and nothing in\nthe bare number says which. Check this before quoting a dimension back to a user or\ninterpreting one they gave you.\n\nNote this reports what the project DISPLAYS. Raw doubles returned by this bridge are\nalways Revit internal units (feet, ft2, ft3, radians) unless a tool says otherwise.\n\nArgs:\n    params.all: every measurable spec, not just the headline ones (default false)\n    params.discipline: only this discipline, e.g. 'Structural', 'HVAC'\n    params.limit: max 400, default 40\n\nReturns:\n    str: JSON with {display_unit_system, total_count, returned, offset, limit,\n         truncated, showing, disciplines, units: [{spec, spec_type_id, discipline,\n         headline, unit, unit_type_id, symbol, accuracy, use_digit_grouping,\n         suppress_trailing_zeros, suppress_leading_zeros, uses_project_default}]}"
    },
    {
      "name": "revit_get_view_templates",
      "description": "View templates, what each one controls, and which views use it.\n\nA parameter listed in `controlled_parameters` is locked by the template - changing it\non a view that uses the template will not stick. Pass template_id for one template\nand the response also lists every category that template hides, which is the usual\nexplanation for \"why can't I see X in this view\".\n\nArgs:\n    params.template_id: report one template in detail (omit for the list)\n    params.limit: max 500, default 50\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, view_templates:\n         [{id, name, view_type, scale, detail_level, discipline,\n         controlled_parameter_count, controlled_parameters, views_using_count,\n         views_using, hidden_categories}]}"
    },
    {
      "name": "revit_get_views",
      "description": "List all non-template views in the Revit model (floor plans, sections, 3D, etc.).\n\nReturns:\n    str: JSON array of {id, name, view_type}"
    },
    {
      "name": "revit_get_warnings",
      "description": "Get all Revit model warnings and errors with descriptions and affected element IDs.\n\nReturns:\n    str: JSON array of {description, severity, failing_elements: [element_id, ...]}"
    },
    {
      "name": "revit_get_worksets",
      "description": "List all user worksets in a workshared Revit model. Returns workshared=false if not workshared.\n\nReturns:\n    str: JSON with {workshared, worksets: [{id, name, is_open, is_editable}]}"
    },
    {
      "name": "revit_hide_elements",
      "description": "Permanently hide elements or whole categories in one Revit view ('Hide in View').\n\nThis is a real, saved change to the view - not temporary isolate. It is\nundoable in Revit as a single step, but the user will notice, so run it with\ndry_run=True first when you are not certain of the element set.\n\nFor a throwaway look at a subset, prefer revit_isolate_elements.\n\nArgs:\n    params.element_ids: Elements to hide. Omit with categories to use the current selection.\n    params.categories: Whole categories to hide, e.g. ['Furniture']\n    params.view_id / params.view_name: Which view. Omit for the active view.\n    params.dry_run: Report what would be hidden and change nothing\n\nReturns:\n    str: JSON with {dry_run, hidden|would_hide: {view, elements_to_hide,\n         categories_to_hide, not_found, cannot_be_hidden, categories_skipped}}"
    },
    {
      "name": "revit_import_parameters_csv",
      "description": "Write a CSV of parameter values back into the model - the return leg of revit_export_elements_csv.\n\nThe CSV's first column is ElementId; every other column is a parameter name.\nThat is exactly the shape revit_export_elements_csv writes, so the usual\nworkflow is: export, edit in Excel, import. Until now that was a one-way trip.\n\nThe whole file is validated first - missing elements, duplicate ids, unknown\nor read-only parameters, unparseable values - and every conflict is reported\nBEFORE anything is written. dry_run defaults to TRUE for that reason; read\nthe plan, then re-run with dry_run=false.\n\nThe metadata columns export writes (Category, FamilyAndType, Workset, Level)\nare skipped by default since they are not writable parameters.\n\nArgs:\n    params.csv_path: the file to read\n    params.dry_run: default True - validate and report, write nothing\n    params.allow_partial: write the good cells even when the file has conflicts\n    params.units / params.column_units: what unit numeric length columns are in\n\nReturns:\n    str: JSON with {plan{columns, rows_read, conflicts{...}}, summary, changes,\n         problems, undo}"
    },
    {
      "name": "revit_isolate_elements",
      "description": "Temporarily isolate elements or categories in the active Revit view.\n\nTemporary isolate, the same as the Revit toolbar button - it does not change\nthe saved view and revit_reset_temporary_view puts everything back. Use it\nto clear a busy view before capturing an image.\n\nPass element_ids, or categories, or neither to isolate the current selection.\nElements win if both are given, because Revit's isolate set is one or the other.\n\nArgs:\n    params.element_ids: Elements to isolate\n    params.categories: Categories to isolate, e.g. ['Structural Framing']\n\nReturns:\n    str: JSON with {mode, source, view, isolated, not_found, transaction, note}"
    },
    {
      "name": "revit_layout_views_on_sheet",
      "description": "Place several views on one sheet in a grid, row or column, inside the titleblock's usable area.\n\nThe usable area is the titleblock's own extent on the sheet, inset by\n`margin`, with `title_strip_width` reserved along `title_strip_side`. Revit\ndoes not publish where a titleblock's text strip sits, so set\ntitle_strip_width to your titleblock's strip if you do not want drawings\nrunning under it, or pass usable_area to set the rectangle outright. A sheet\nwith no titleblock falls back to the sheet's own outline, and the response\nsays which source was used.\n\nAfter each viewport is placed its real size is measured against its cell, and\nanything that overflows is reported in warnings with its actual dimensions,\nso an overfull sheet is never silently wrong.\n\nRun dry_run=true first to see the grid and every cell centre. All placements\ncommit as one undo step. Uses the same version-dependent positioning as\nrevit_place_view_on_sheet and reports it in placement_path.\n\nReturns:\n    str: JSON with {sheet, layout_info: {layout, columns, rows,\n         usable_area_source, usable_area, cell_size, margin, gutter,\n         title_strip_width, title_strip_side, units}, placement_path,\n         placed_count, failed, failures, warnings,\n         placements: [{row, column, fits_in_cell, viewport}]}"
    },
    {
      "name": "revit_list_instances",
      "description": "List every running Revit instance that has an MCP bridge loaded.\n\nEach Revit version binds its own port, so more than one can be live at once.\nUse this to see what is available and which one the other tools are currently\npointed at, then revit_select_instance to switch.\n\nReturns:\n    str: JSON with {instances: [{revit_version, port, process_id, document,\n         has_document, active}], active_port, pinned}"
    },
    {
      "name": "revit_load_family",
      "description": "Load one or more .rfa family files into the open project.\n\nReports the family and every type it brought in, so the type ids are\nimmediately usable with revit_place_family_instance. Loading a family that\nis already in the model RELOADS it, replacing the definition - the response\nflags that case, and dry_run=true tells you in advance.\n\nArgs:\n    params.path or params.paths: the .rfa file(s)\n    params.overwrite_parameter_values: on reload, whose type values win\n    params.dry_run: check paths and already-loaded state without loading\n\nReturns:\n    str: JSON with {families: [{path, loaded, reloaded, family_id, family_name,\n         category, type_count, types: [{type_id, type_name}]}], undo}"
    },
    {
      "name": "revit_mirror_elements",
      "description": "Mirror elements across a plane, in a single transaction and a single undo step.\n\nA dry run reports Revit's own verdict on whether the selection can be\nmirrored at all (CanMirrorElements), which is the usual reason a mirror\nfails - typically a selection that mixes hosted elements with their hosts.\n\nArgs:\n    params.element_ids / params.filter: what to mirror\n    params.units: unit for the plane origin (required)\n    params.plane, params.origin: the mirror plane\n    params.copy: True (default) mirrors copies; False mirrors in place\n    params.dry_run: preview only\n\nReturns:\n    str: JSON with {plane, copy, created_count, created_ids, undo}"
    },
    {
      "name": "revit_move_element",
      "description": "Move a Revit element by a delta vector (dx, dy, dz) in feet.\n\nArgs:\n    params.element_id: ElementId to move\n    params.dx, dy, dz: Movement vector in feet\n\nReturns:\n    str: JSON with {element_id, moved_by: {dx, dy, dz}}"
    },
    {
      "name": "revit_move_elements",
      "description": "Move many elements by one vector, in a single transaction and a single undo step.\n\nThe bulk form of revit_move_element, and unlike it, this one requires the\nunit the vector is expressed in and echoes back what was applied in feet.\nA dry run also reports how many of the matched elements are pinned.\n\nArgs:\n    params.element_ids / params.filter: what to move\n    params.units: unit for the vector (required)\n    params.translation: {dx, dy, dz}\n    params.dry_run: preview only\n\nReturns:\n    str: JSON with {translation{supplied, units, applied_ft}, moved_count,\n         moved_ids, failures, undo}"
    },
    {
      "name": "revit_override_element_graphics",
      "description": "Override colour, line weight, surface pattern or transparency for elements in one Revit view.\n\nUse it to make a query result visible: colour the twelve walls that fail a\ncheck red, then capture the view. A plain `color` sets both line and surface\ncolour and applies a solid fill, so it reads in shaded and hidden-line views\nalike.\n\nThis changes what the user sees. Run dry_run=True first if you are unsure of\nthe set, and reset=True clears the overrides completely afterwards.\n\nArgs:\n    params.element_ids / params.categories: What to override. Omit both to use the selection.\n    params.color: '#RRGGBB' or 'r,g,b'\n    params.line_color / params.surface_color: finer control than `color`\n    params.surface_pattern: 'solid', 'none', or a fill pattern name\n    params.line_weight: 1-16\n    params.transparency: 0-100\n    params.halftone: draw halftone\n    params.reset: clear all overrides on these elements\n    params.dry_run: report the change without making it\n\nReturns:\n    str: JSON with {dry_run, changed|would_change: {view, changes, element_count,\n         categories, not_found, elements}, failed}"
    },
    {
      "name": "revit_pin_elements",
      "description": "Pin many elements at once - a guardrail for what an agent must not touch.\n\nPinning is a real protection here, not just a note: revit_delete_elements\nrefuses to delete pinned elements unless include_pinned is set explicitly.\nA BIM manager can pin the structural grid, the levels, the survey point and\nanything else that should survive, then let an agent work.\n\nArgs:\n    params.element_ids / params.filter: what to pin\n    params.dry_run: preview only\n\nReturns:\n    str: JSON with {target_state, changed, unchanged, failed, failures, undo}"
    },
    {
      "name": "revit_place_family_instance",
      "description": "Place a loaded family type - a door, a column, a beam, a piece of equipment.\n\nSupports Revit's real placement flavours rather than only free points:\n  point  - free-standing at an XYZ\n  level  - associated with a level\n  host   - inserted into a host element; this is how a door goes into a wall\n           (pass host_id = the wall's ElementId)\n  face   - on the nearest face of the host element to your point\n  curve  - along start -> end, for beams and braces (with structural_type)\n\nThe family symbol is activated automatically if it is not already active,\nwhich is the usual reason a placement silently does nothing.\n\nCoordinates are in `units` and the response echoes both what you supplied and\nwhat was applied in feet. Use revit_get_families to find loaded families, or\nrevit_load_family first.\n\nArgs:\n    params.type_id or family_name/type_name: which type to place\n    params.units: unit for every coordinate (required)\n    params.location or params.locations: where\n    params.host_id / params.level: what it belongs to\n    params.dry_run: resolve everything and report the plan, creating nothing\n\nReturns:\n    str: JSON with {mode, symbol, level, host, points, created[{element_id, host_id}],\n         parameter_results, undo}"
    },
    {
      "name": "revit_place_view_on_sheet",
      "description": "Place a view on a sheet as a viewport at a given point, and report the viewport id and where it actually landed.\n\nPositioning is version-dependent, and the response names the path that ran:\n  - Revit 2026+ uses ViewAnchor with Viewport.SetViewAnchorAndPosition, so\n    Revit itself puts the chosen anchor on the point, and an optional named\n    ViewPosition element can be attached to keep the view aligned across\n    sheets.\n  - Revit 2024/2025 have no anchor API at all. The bridge centres the\n    viewport with SetBoxCenter, regenerates, reads the real box outline back\n    with GetBoxOutline and shifts the centre by the measured delta, so the\n    requested corner still lands on the point.\n\nA view can live on only one sheet. If it is already placed, the error names\nthe sheet holding it; duplicate the view first if you need it twice.\n\nReturns:\n    str: JSON with {sheet, requested: {anchor, point}, placement_path,\n         viewport: {viewport_id, view_id, view_name, box_center, box_min,\n         box_max, box_width, box_height, rotation}, units, warnings}"
    },
    {
      "name": "revit_populate_titleblock",
      "description": "Set sheet and titleblock parameters across a set of sheets in one undoable operation.\n\nEach parameter is looked for on the sheet first and then on its titleblock\ninstance (configurable with `target`), because project titleblocks put the\nsame field in either place. Read-only parameters and parameters that do not\nexist are reported per sheet rather than aborting the run.\n\ndry_run=true shows, for every sheet, where each parameter was found, whether\nit is writable, and its current value.\n\nText parameters are written verbatim; numeric parameters are written in\nRevit's internal units (feet, radians).\n\nReturns:\n    str: JSON with {dry_run, sheet_count, values_written|would_write, failed,\n         failures, sheets: [{sheet_id, SheetNumber, sheet_name, parameters}]}"
    },
    {
      "name": "revit_purge_unused",
      "description": "Purge unused families, types, materials and other content. DESTRUCTIVE - dry run by default.\n\ndry_run defaults to True and changes nothing: it returns exactly what would be\ndeleted, grouped by category with counts and example names. Review that list,\nthen call again with dry_run=False.\n\nA real purge runs in one TransactionGroup, so a single Ctrl+Z in Revit reverses\nall of it - but only until the file is saved and the undo history is discarded.\n\nReturns:\n    str: JSON with {dry_run, would_delete or deleted, groups: [{category, count,\n                    sample_names, sample_ids}], failures, elapsed_ms}"
    },
    {
      "name": "revit_query_elements",
      "description": "Find elements in the Revit model by any combination of filters. START HERE for\nalmost any \"which elements...\" question - this one tool replaces a dozen narrow ones.\n\nFILTERS (combine freely; all are ANDed):\n  category      'Walls', or ['Doors', 'Windows'], or a BuiltInCategory like 'OST_Walls'\n  level         level name or id, e.g. 'L2'\n  workset       workset name or id (workshared models only)\n  phase         phase name or id - matches elements CREATED in that phase\n  family        family name(s), exact, case-insensitive\n  type_name     type name(s), exact, case-insensitive\n  parameter_filters  list of {name, operator, value} - see the operator table below\n  bounding_box  {min:{x,y,z}, max:{x,y,z}, units} - elements intersecting the box\n  element_ids   restrict to a set you already have\n\nPARAMETER FILTER OPERATORS:\n  equals         exact match, case-insensitive\n  not_equals     inverse of equals\n  contains       substring match, case-insensitive\n  starts_with    prefix match, case-insensitive\n  greater_than   numeric, on the stored value\n  less_than      numeric, on the stored value\n  is_empty       parameter absent, unset, or an all-whitespace string (omit `value`)\n  is_not_empty   parameter present and non-blank (omit `value`)\n\nEach rule is checked against the INSTANCE parameter and falls back to the TYPE\nparameter of the same name, so a rule finds elements whether the parameter is\ndefined per-instance or per-type. Set parameter_type_fallback=false for instance-only.\n\nValue semantics, which matter:\n  * A STRING value is compared against how the parameter DISPLAYS - the same text\n    the user sees in Revit. So Level equals 'L2', or Height equals \"8' 0\"\".\n  * A NUMBER value is compared against the STORED value, which for any length is\n    decimal FEET, for an area ft2, a volume ft3, an angle radians. `{\"name\":\n    \"Volume\", \"operator\": \"greater_than\", \"value\": 15}` means 15 cubic feet. This\n    catches people out: a beam type named 'CB24x24' stores b = 2.0 and h = 2.0,\n    because those 24 inches are 2 feet. Call revit_get_units to see what the\n    project DISPLAYS, and prefer a string value when you want display semantics.\n\nWHY THE TYPE FALLBACK MATTERS, from a real structural model: a beam INSTANCE\ncarries Mark, Comments, Volume, Reference Level and Cut Length, while Fire Rating,\nType Mark, Manufacturer, Assembly Code and the section dimensions b and h live on\nits TYPE. A rule that only looked at instance parameters would silently find none\nof the second group. Leave parameter_type_fallback on unless you mean instance-only.\n\nOUTPUT SHAPING:\n  fields    pick exactly the columns you want instead of getting everything. Built-in\n            names: id, unique_id, name, category, category_id, type_id, type_name,\n            family_name, level, level_id, workset, workset_id, phase_created,\n            phase_demolished, design_option, group_id, assembly_id, host_id, is_type,\n            bbox, location, volume, area, length. Any OTHER name is read as a\n            parameter name, so fields=['id','Mark','WORK PACKAGE'] returns those as\n            columns. Defaults to [id, name, category, type_name].\n  units     ft / in / mm / cm / m for every distance in the response.\n  limit     max 1000, default 100.  offset for paging.\n  count_only  returns just the number. Run this FIRST on a query you expect to be\n            large - it is far cheaper than fetching a page you will discard.\n\nThe response always carries total_count (the true number of matches, not the page\nsize) and truncated, so you can tell whether you have seen everything.\n\nPerformance: category, level, workset, phase, bounding_box and element_ids are\npushed into Revit's own filters. parameter_filters are checked per candidate, so\nalways pair them with a category or level on a large model.\n\nEXAMPLES (parameter and family names below are taken from a real structural\nmodel, so they show the shape of genuine Revit data):\n\n  concrete beams on level L2, as a table of id / type / volume:\n    {\"category\": \"Structural Framing\", \"level\": \"L2\",\n     \"family\": \"Concrete-Rectangular Beam\",\n     \"fields\": [\"id\", \"type_name\", \"volume\"]}\n\n  how many framing members are missing a Mark:\n    {\"category\": \"Structural Framing\", \"count_only\": true,\n     \"parameter_filters\": [{\"name\": \"Mark\", \"operator\": \"is_empty\"}]}\n\n  every W-section beam - one model really does contain both 'W12X26' and\n  'W18x55', which is why every name match here is case-insensitive:\n    {\"category\": \"Structural Framing\",\n     \"parameter_filters\": [{\"name\": \"Type Name\", \"operator\": \"starts_with\", \"value\": \"W\"}]}\n\n  walls whose TYPE carries Type Mark 'CW10', returned with that parameter as a\n  column - the type fallback finds them even though no wall INSTANCE defines it:\n    {\"category\": \"Walls\", \"fields\": [\"id\", \"type_name\", \"Type Mark\"],\n     \"parameter_filters\": [{\"name\": \"Type Mark\", \"operator\": \"equals\", \"value\": \"CW10\"}]}\n\n  beams over 15 cubic feet, sizes reported in millimetres:\n    {\"category\": \"Structural Framing\", \"units\": \"mm\",\n     \"fields\": [\"id\", \"type_name\", \"volume\", \"bbox\"],\n     \"parameter_filters\": [{\"name\": \"Volume\", \"operator\": \"greater_than\", \"value\": 15}]}\n\nReturns:\n    str: JSON with {total_count, returned, offset, limit, truncated, units, fields,\n         notes, elements: [{...requested fields...}]}, or {count, count_only} when\n         count_only is set."
    },
    {
      "name": "revit_rename_views",
      "description": "Bulk-rename views, from an explicit list or by find/replace across view names.\n\nEvery target name is checked for collisions first - against views outside the\nbatch and against other renames inside it - and collisions are judged per view\nfamily, the scope Revit enforces. If any collision exists nothing is written\nat all, so a batch never half-applies. Run dry_run=true to see the full\nbefore/after list.\n\nThe Project Browser and System Browser pseudo-views are never included.\nAll the renames commit in one transaction group, so a single Ctrl+Z in Revit\nreverses the whole batch.\n\nReturns:\n    str: JSON with {dry_run, renamed|planned_count, conflict_count, conflicts,\n         renames: [{view_id, view_type, old_name, new_name, blocked}]}"
    },
    {
      "name": "revit_renumber_sheets",
      "description": "Renumber a set of sheets by a prefix/start/increment rule or an explicit map. Dry run by default.\n\ndry_run defaults to TRUE. Collisions are detected before anything is written:\na new number another sheet outside the batch already holds, or two sheets in\nthe batch landing on the same number, blocks the whole run so it never\nhalf-applies.\n\nWhen a new number is currently held by another sheet inside the batch - a\nshuffle such as S100 to S101 to S102 - the write parks every sheet on a\ntemporary number first, so no intermediate collision occurs. The dry run\nreports this as two_phase_required.\n\nThe whole renumbering is one undo step.\n\nReturns:\n    str: JSON with {dry_run, rule, renumbered|planned_count, conflict_count,\n         conflicts, two_phase_required, renumbering: [{sheet_id, sheet_name,\n         old_number, new_number, unchanged, blocked}]}"
    },
    {
      "name": "revit_reset_temporary_view",
      "description": "Clear temporary hide/isolate (and reveal-hidden mode) in the active Revit view.\n\nThe undo for revit_isolate_elements. Safe to call when nothing is isolated -\nit reports changed=false.\n\nReturns:\n    str: JSON with {view, changed, cleared_hide_isolate, cleared_reveal_hidden}"
    },
    {
      "name": "revit_resolve_warnings",
      "description": "Bulk-resolve the warning classes that are safe to handle automatically. DESTRUCTIVE - dry run by default.\n\nCurrently one class is auto-handled: 'duplicate_instances' (Revit's \"there are\nidentical instances in the same place\"), where every copy but the lowest\nElementId is deleted. Warnings that need human judgement are deliberately\nrefused, not guessed at - the response returns a catalogue of every warning\nclass in the model saying which are auto-handled and why the rest are not.\n\nPinned elements and group members are skipped with a reason unless explicitly\nallowed. dry_run defaults to True and returns the exact keep/delete plan.\n\nReturns:\n    str: JSON with {dry_run, would_resolve_warnings, would_delete_elements,\n                    plan: [{warning_index, description, keep_element_id,\n                            delete_element_ids}], skipped, warning_catalogue}"
    },
    {
      "name": "revit_room_compliance_check",
      "description": "Check rooms against caller-supplied area, height and occupancy rules.\n\nReports pass/fail per room with the actual measured values alongside the\nexpected ones, so a failure can be checked rather than taken on trust.\nUnplaced and unbounded rooms cannot be evaluated and are reported separately.\n\nOccupancy is computed as area / occupant_load_factor (rounded up); Revit's own\nOccupancy parameter is free text and is reported alongside, not tested against.\nRoom volume is only available when the model has volume computation enabled -\nthe response says which case you are in.\n\nReturns:\n    str: JSON with {area_unit, length_unit, volumes_computed_in_model, total_rooms,\n                    rooms_evaluated, rooms_passing, rooms_failing, rooms_not_evaluable,\n                    failures_by_rule, rooms: [{id, number, name, level, area,\n                    unbounded_height, computed_occupancy, passed,\n                    failures:[{rule, requirement, expected, actual, unit}]}], limitations}"
    },
    {
      "name": "revit_rotate_elements",
      "description": "Rotate many elements about an axis, in a single transaction and a single undo step.\n\nAngles are in degrees by default and converted to radians at the bridge; the\nresponse echoes the supplied angle, its unit, and the applied radians. The\naxis defaults to vertical through `axis_point`.\n\nArgs:\n    params.element_ids / params.filter: what to rotate\n    params.units: unit for the axis point (required)\n    params.angle, params.angle_units: how far\n    params.axis_point, params.axis_direction: about what\n    params.dry_run: preview only\n\nReturns:\n    str: JSON with {axis, angle{supplied, units, applied_radians},\n         rotated_count, rotated_ids, failures, undo}"
    },
    {
      "name": "revit_select_instance",
      "description": "Point every other Revit tool at a specific running Revit instance.\n\nSelect by version (\"2027\") or by port. Pass release=True to clear the pin and\ngo back to automatic selection, which prefers an instance with a model open\nand then the newest Revit version.\n\nArgs:\n    params.revit_version: Revit version to target, e.g. '2024'\n    params.port: Bridge port to target, if you know it\n    params.release: Clear the pin and return to automatic selection\n\nReturns:\n    str: JSON with {selected, revit_version, document, pinned}"
    },
    {
      "name": "revit_set_active_view",
      "description": "Open a view or sheet in Revit by id or by name.\n\nSheets also match on sheet number ('A101') or 'A101 - Level 1 Plan'.\n\nArgs:\n    params.view_id: ElementId of the view or sheet\n    params.view_name: View name, sheet number, or 'number - name'\n\nReturns:\n    str: JSON with {changed, opened: {...}, previous_view: {...}}"
    },
    {
      "name": "revit_set_crop_region",
      "description": "Set a view's crop box explicitly, drive it from a scope box, or reset it.\n\nA Revit crop box lives in the view's own coordinate frame, not model\ncoordinates. Pass coordinate_space='model' to give model coordinates and let\nthe bridge transform them into that frame.\n\nmode='reset' clears the scope box link, removes any non-rectangular crop\nshape and switches cropping off.\n\nReturns:\n    str: JSON with {view, mode, actions, warnings, crop_active, crop_visible,\n         scope_box_id, crop_box: {min, max}, units}"
    },
    {
      "name": "revit_set_parameter",
      "description": "Set a parameter value on a Revit element. Supports string, integer, and double parameters.\n\nArgs:\n    params.element_id: Target element's integer ElementId\n    params.parameter_name: Exact parameter name (case-sensitive)\n    params.value: New value as a string (auto-cast to the correct type)\n\nReturns:\n    str: JSON with {element_id, parameter, new_value} on success"
    },
    {
      "name": "revit_set_parameters_by_filter",
      "description": "Apply parameter values to everything matching a query, without listing any ids.\n\nSame engine and same single-undo-step guarantee as revit_batch_set_parameters;\nthis one takes only a filter, so \"set WORK PACKAGE on every conduit on Level 2\"\nis one call with no id round trip first.\n\nRun it once with dry_run=true to see the match count and the per-element plan\nbefore committing.\n\nArgs:\n    params.filter: category / level / workset / type / family / name query\n    params.parameters: the assignments\n    params.dry_run: preview only; no transaction is opened\n\nReturns:\n    str: JSON with {selector, elements_matched, summary, results, problems, undo}"
    },
    {
      "name": "revit_set_selection",
      "description": "Select elements in Revit by ElementId, so the user sees exactly what you mean.\n\nIds that do not exist come back in not_found rather than being silently\ndropped - a missing id usually means the query results you are working from\nare stale.\n\nArgs:\n    params.element_ids: ElementIds to select\n    params.add: Add to the current selection instead of replacing it\n\nReturns:\n    str: JSON with {mode, requested, resolved, selected_count, not_found, elements}"
    },
    {
      "name": "revit_snapshot_create",
      "description": "Capture the current state of the Revit model to disk so it can be diffed later.\n\nRecords per element: id, category, family and type, level, workset, phase,\na bounding-box centroid and a stable hash of its parameter values. Also\nrecords document-level totals: element counts by category, warning count and\nwarning types, view and sheet counts, worksets and links.\n\nTake one BEFORE letting an agent edit a model and one after; revit_snapshot_diff\nthen shows exactly what the agent changed. This is the audit trail that makes\nagentic editing reviewable.\n\nThe model is never modified. Snapshots are written as gzipped JSON under\n%LOCALAPPDATA%\\CampbellCTS\\RevitMcp\\snapshots\\<project>\\, never into the\nproject folder. Roughly 4 MB per 200k elements at detail='hash' and ~37 MB at\ndetail='full'.\n\nCapture stops at max_elements or time_budget_ms, whichever comes first, and\nsays so in `truncated` / `truncation_reason` -- check those before trusting a\ndiff built on the snapshot.\n\nArgs:\n    params.label: Short label, becomes part of the snapshot id\n    params.detail: 'hash' (default), 'full' for before/after parameter values, or 'summary'\n    params.categories: Optional category scope for the element capture\n    params.include_geometry: Record bounding-box centroids so moves can be detected\n    params.include_types: Also capture element types, not just instances\n    params.max_elements / params.time_budget_ms: Capture bounds\n\nReturns:\n    str: JSON with {snapshot_id, label, detail, project, path, meta_path,\n         element_count, size_bytes, truncated, truncation_reason, capture_ms,\n         elapsed_ms, document_totals, note}"
    },
    {
      "name": "revit_snapshot_delete",
      "description": "Delete or prune saved model snapshots. Runs as a dry run unless dry_run=False.\n\nDestructive: deleted snapshots cannot be recovered, and any audit trail built\non them is lost with them. It is a dry run by default, and it refuses to do\nanything without an explicit snapshot_id, older_than_days or keep_latest --\nthere is deliberately no \"delete everything\" shorthand. Only snapshot files\nunder %LOCALAPPDATA%\\CampbellCTS\\RevitMcp are ever touched; the Revit model\nis not affected.\n\nCall it once with the defaults, show the caller what would go, and only then\ncall again with dry_run=False.\n\nArgs:\n    params.dry_run: True (default) reports without deleting\n    params.snapshot_id / params.snapshot_ids: Specific snapshots to remove\n    params.older_than_days: Remove snapshots older than this\n    params.keep_latest: Keep this many newest per project, remove the rest\n    params.project / params.all_projects: Scope\n\nReturns:\n    str: JSON with {dry_run, projects, matched, deleted, bytes_freed,\n         unresolved_ids, snapshots: [{snapshot_id, path, size_bytes, deleted}], note}"
    },
    {
      "name": "revit_snapshot_diff",
      "description": "Compare two model snapshots and report exactly what changed between them.\n\nReports added, removed and modified elements. For modified elements it names\nthe specific parameters that changed with before and after values -- provided\nBOTH snapshots were captured with detail='full'; otherwise the element is still\nreported as modified (via its parameter hash) and the response says plainly\nthat parameter detail is unavailable. Elements that moved are reported with\nthe distance in feet.\n\nAlso diffs the document-level totals, so element-count and warning-count drift\nis visible at a glance even when the element lists are long.\n\nRead `summary` and `by_category` first; the per-element lists are capped by\n`limit` while the counts are always exact.\n\nArgs:\n    params.from_snapshot / params.to_snapshot: Snapshot ids, or 'latest' / 'previous' / 'oldest'\n    params.project: Project to diff within, if not the open model\n    params.categories: Restrict the diff to these categories\n    params.limit: Max elements listed per bucket\n    params.max_parameter_changes: Max named parameter changes per modified element\n\nReturns:\n    str: JSON with {from, to, summary: {added, removed, modified, unchanged},\n         by_category, totals_diff: {element_count, warning_count, by_category,\n         warnings_by_type, ...}, parameter_detail_available, added, removed,\n         modified: [{id, category, type, moved, changes: [{parameter, from, to}]}],\n         truncated, notes}"
    },
    {
      "name": "revit_snapshot_list",
      "description": "List saved model snapshots for the open project (or every project), newest first.\n\nUse this to find the snapshot ids that revit_snapshot_diff and\nrevit_get_recent_changes take, and to see how much disk the snapshot history\nis using before pruning it.\n\nArgs:\n    params.all_projects: List every project's snapshots\n    params.project: A specific project name, if not the open model\n    params.label: Filter to snapshots whose id or label contains this text\n    params.limit: Max snapshots to return (default 50)\n\nReturns:\n    str: JSON with {root, projects, total_snapshots, returned, total_size_bytes,\n         snapshots: [{snapshot_id, label, created_utc, detail, element_count,\n         truncated, size_bytes, revit_version, document_title, warning_count, path}]}"
    },
    {
      "name": "revit_suggest_name_corrections",
      "description": "Propose corrected names for naming violations, and optionally apply them. DESTRUCTIVE - dry run by default.\n\nTakes the same rules as revit_audit_naming plus a \"fix\" spec per rule, and\nreturns the current name, the suggested name, and whether the suggestion\nactually satisfies the rule. Suggestions that are unchanged, that still break\ntheir rule, or that collide with an existing name are marked and not applied.\n\ndry_run defaults to True. A real run renames inside one TransactionGroup, so a\nsingle Ctrl+Z reverses all of it.\n\nReturns:\n    str: JSON with {dry_run, total_violations, would_rename or renamed,\n                    suggestions: [{target, id, current_name, suggested_name, rule,\n                                   reason, suggestion_passes_rule, name_collision,\n                                   will_apply}], failures}"
    },
    {
      "name": "revit_tag_all_rooms",
      "description": "Place a room tag centred on every room in a view.\n\nTags go on the room's own location point, which Revit keeps inside the room.\nRooms that are unplaced or not enclosed (zero area) are skipped and listed\nwith the reason, because tagging them would produce a tag with nothing in it.\n\nCommits as one undo step, with Revit warnings suppressed so an unattended\ndialog cannot block the bridge.\n\nReturns:\n    str: JSON with {dry_run, view, rooms_in_view, tags_placed|would_tag,\n         skipped_count, skipped, failed, failures, tags: [{tag_id, room_id,\n         room_name, room_number, position}]}"
    },
    {
      "name": "revit_tag_elements_in_view",
      "description": "Tag every untagged element of the given categories in one view, with a chosen tag type, leader and orientation.\n\nElements already carrying a tag in that view are skipped, so the tool is safe\nto re-run after adding model elements - it only fills the gaps.\n\nIf no tag type is given, the bridge finds the tag category from the model\ncategory the way Revit names them (Structural Columns to Structural Column\nTags) and uses the first loaded type. A category with no tag family loaded is\nreported per category rather than failing the whole call.\n\nEvery tag is its own transaction inside one transaction group, so one\nstubborn element cannot lose the rest of the batch, and a single Ctrl+Z still\nreverses the lot. Revit warnings raised during tagging are suppressed rather\nthan raising a dialog that would block the bridge.\n\nRun dry_run=true first to see the counts per category.\n\nReturns:\n    str: JSON with {dry_run, view, tags_placed|would_tag, failed, failures,\n         leader, orientation, categories: [{category, in_view, already_tagged,\n         to_tag, tag_type, error}], tags: [{tag_id, tagged_element_id,\n         category, head_position}]}"
    },
    {
      "name": "revit_unhide_elements",
      "description": "Unhide elements or categories previously hidden in a Revit view.\n\nThe undo for revit_hide_elements. Elements that were not hidden are reported\nin `failed` rather than taking the whole call down with them.\n\nArgs:\n    params.element_ids: Elements to unhide\n    params.categories: Categories to unhide\n    params.view_id / params.view_name: Which view. Omit for the active view.\n    params.dry_run: Report what would be unhidden and change nothing\n\nReturns:\n    str: JSON with {dry_run, view, unhidden, categories, not_found, failed}"
    },
    {
      "name": "revit_unpin_elements",
      "description": "Release the pin on many elements at once, in one undo step.\n\nThe inverse of revit_pin_elements. Unpinning removes the protection that\nmakes revit_delete_elements skip an element, so prefer a dry run first.\n\nArgs:\n    params.element_ids / params.filter: what to unpin\n    params.dry_run: preview only\n\nReturns:\n    str: JSON with {target_state, changed, unchanged, failed, failures, undo}"
    },
    {
      "name": "revit_validate_door_to_room",
      "description": "Validate all doors have valid from-room / to-room assignments for room data schedules.\n\ntotal_doors and invalid_doors count the whole model; doors is the requested\npage of that list.\n\nReturns:\n    str: JSON with {total_doors, invalid_doors, total, returned, offset, limit,\n                    truncated,\n                    doors: [{door_id, door_name, from_room, to_room, is_valid}]}"
    },
    {
      "name": "revit_zoom_to_elements",
      "description": "Zoom the active Revit view to fit the given elements (or the current selection).\n\nPair this with revit_capture_view_image to actually look at something you\nfound by query: zoom to it, then capture the view.\n\nArgs:\n    params.element_ids: Elements to frame. Omit to use the current selection.\n    params.select: Also select them so they highlight (default True)\n    params.padding: Margin around the elements as a fraction of their size\n    params.show_elements: Let Revit hunt for a view that shows them. Off by\n        default because it can open a modal dialog that stalls the bridge.\n\nReturns:\n    str: JSON with {view, source, requested, resolved, with_bounding_box,\n         not_found, selected, zoomed, bounding_box, note}"
    }
  ]
}
