ifcmcp: source tool descriptions from ifcquery/ifcedit instead of duplicating them

Alternative to #8955, for #8951 (23 of 25 ifcmcp tools reach MCP clients
with an empty description because FastMCP reads each wrapper's own
__doc__, and the server.py wrappers had none).

#8955 fixes this by hand-writing a new docstring directly onto each
server.py wrapper. Most of those wrappers are thin passthroughs to
IfcSession methods in core.py, which already had short docstrings, which
themselves mostly delegate to already-documented ifcquery/ifcedit
functions -- so that fix tripled up content across three layers that can
drift out of sync.

This instead enriches the true source (the ifcquery/ifcedit library
functions, useful independently of MCP) and has core.py's IfcSession
methods copy __doc__ from their delegate via a small _use_doc()
decorator, and server.py's tool registration pull description= from the
matching IfcSession method. Methods that aren't pure passthroughs
(session lifecycle, generic API/shape dispatch) keep their own
hand-written docs. Keeps #8955's regression test.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Bruno Postle
2026-08-02 14:45:20 +01:00
committed by Thomas Krijnen
parent e077390e3d
commit 6f3acc84ee
15 changed files with 303 additions and 89 deletions
+16 -3
View File
@@ -102,13 +102,26 @@ def clash(
tolerance: float = 0.002,
scope: str = "storey",
) -> dict[str, Any]:
"""Check element for geometric clashes against other elements.
"""Check one element for geometric clashes against other elements.
Reports hard intersections and, optionally, violations of a required
clearance. Returns the overall ``pass``, the ``scope`` actually used, a
``checks`` block in which each clash names the other ``element``, the
clash ``type``, the ``distance`` and the two closest points ``p1``/``p2``,
and a de-duplicated flat ``elements`` list of everything involved.
Geometry is computed for every element in scope, so this is slow on large
models; ``pass`` is ``None`` with an ``error`` when the element has no
usable geometry.
:param model: The IFC model.
:param element: The element to check.
:param clearance: Minimum clearance distance; if provided, runs clearance check.
:param clearance: Minimum required clearance distance; when given, also
runs the clearance check alongside the intersection check.
:param tolerance: Intersection tolerance in meters (default 0.002).
:param scope: Which elements to check against: "storey" or "all".
:param scope: ``"storey"`` (default) checks only elements sharing the
same spatial container; ``"all"`` checks every ``IfcElement``.
``"storey"`` falls back to ``"all"`` when the element has no spatial
container.
:return: Dict with clash results suitable for JSON serialization.
"""
result: dict[str, Any] = {"element": _ref(element)}
+12 -3
View File
@@ -26,10 +26,19 @@ def _cost_item_to_dict(item: ifcopenshell.entity_instance, max_depth: int | None
def cost(model: ifcopenshell.file, max_depth: int | None = None) -> list[dict[str, Any]]:
"""Return a list of IfcCostSchedule entries with nested cost item trees.
"""List the cost schedules: bills of quantities and their cost items.
max_depth limits how many levels of subitems are expanded (None = unlimited).
At the cutoff level, subitems is replaced with {"truncated": True, "count": N}.
Covers ``IfcCostSchedule`` only — this is the money dimension of the
model; see ``schedule()`` in this module for the construction programme.
Each cost item reports its cost ``values`` as ``formula`` label and
``category`` pairs, together with its nested ``subitems``. Returns an
empty list when the model has no cost schedules.
:param model: The in-memory IFC model.
:param max_depth: Levels of cost item nesting to expand, counting root
items as level 1. Past the cutoff ``subitems`` is replaced by a
``{"truncated": True, "count": N}`` marker giving the number of items
not expanded. ``None`` (default) expands to unlimited depth.
"""
result = []
for cost_schedule in model.by_type("IfcCostSchedule"):
+10 -1
View File
@@ -188,7 +188,16 @@ def _material_to_dict(material: ifcopenshell.entity_instance | None) -> dict[str
def info(model: ifcopenshell.file, element: ifcopenshell.entity_instance) -> dict[str, Any]:
"""Return deep inspection data for an element."""
"""Inspect a single entity in depth.
Returns the entity's direct ``attributes`` plus, where present,
``property_sets``, ``element_type``, ``material``, ``container``,
``placement`` and ``geometry_summary``. Keys are omitted when the
information is unavailable.
:param model: The in-memory IFC model.
:param element: The entity to inspect.
"""
result: dict[str, Any] = {
"id": element.id(),
"type": element.is_a(),
+7 -1
View File
@@ -22,7 +22,13 @@ import ifcopenshell
def materials(model: ifcopenshell.file) -> list[dict]:
"""Return all materials and material sets from the model.
"""List the materials and material sets defined in the model.
Returns a single list covering ``IfcMaterial`` (with its category),
``IfcMaterialLayerSet`` (each layer's name, thickness, material and
ventilation flag), ``IfcMaterialConstituentSet`` (constituent names,
materials and fractions) and ``IfcMaterialProfileSet`` (profile names and
materials). Every entry carries the step ID of the material entity.
:param model: The in-memory IFC model.
:return: List of dicts covering IfcMaterial, IfcMaterialLayerSet,
+16 -1
View File
@@ -182,7 +182,22 @@ def _collect_elements(data: Any, seen: set[int], result: list[dict[str, Any]]) -
def relations(
model: ifcopenshell.file, element: ifcopenshell.entity_instance, traverse: str | None = None
) -> dict[str, Any] | list[dict[str, Any]]:
"""Return relationships for an element, or hierarchy chain if traverse='up'."""
"""Show how an element relates to the rest of the model.
By default returns a dict whose optional blocks are ``hierarchy`` (parent,
container, aggregate, nest, filled void, voided element), ``children``
(contained, parts, components, openings), ``type_relationship``,
``groups``, ``systems``, ``zones``, ``material``, ``referenced_structures``
and ``connections`` (connected to/from, ports), plus a de-duplicated flat
``elements`` list of everything referenced. Blocks with nothing to report
are omitted.
:param model: The IFC model.
:param element: The element to examine.
:param traverse: Set to ``'up'`` to instead return the chain of ancestors
from the element to ``IfcProject`` as a flat list. Any other value
gives the default behaviour.
"""
if traverse == "up":
return _traverse_up(element)
result = _all_relations(model, element)
+13 -3
View File
@@ -37,10 +37,20 @@ def _task_to_dict(task: ifcopenshell.entity_instance, max_depth: int | None, dep
def schedule(model: ifcopenshell.file, max_depth: int | None = None) -> list[dict[str, Any]]:
"""Return a list of IfcWorkSchedule entries with nested task trees.
"""List the construction programme: work schedules and their task trees.
max_depth limits how many levels of subtasks are expanded (None = unlimited).
At the cutoff level, subtasks is replaced with {"truncated": True, "count": N}.
Covers ``IfcWorkSchedule`` only — this is the time dimension of the
model; see ``cost()`` in this module for the money dimension. Each
schedule lists its tasks recursively, and each task carries its scheduled
``start`` and ``finish``, an ``is_milestone`` flag, the products it
``outputs`` and its ``subtasks``. Returns an empty list when the model has
no work schedules.
:param model: The in-memory IFC model.
:param max_depth: Levels of subtask nesting to expand, counting root tasks
as level 1. Past the cutoff ``subtasks`` is replaced by a
``{"truncated": True, "count": N}`` marker giving the number of tasks
not expanded. ``None`` (default) expands to unlimited depth.
"""
result = []
for work_schedule in model.by_type("IfcWorkSchedule"):
+9 -1
View File
@@ -8,7 +8,15 @@ import ifcopenshell.util.doc
def schema(model: ifcopenshell.file, entity_type: str) -> dict[str, Any]:
"""Return IFC class documentation for entity_type from model's schema version."""
"""Look up the IFC documentation for an entity class.
Returns the class ``description``, its ``predefined_types``, per-attribute
documentation and a ``spec_url``, resolved against the model's schema
version. Returns an ``error`` key for an unknown class.
:param model: The in-memory IFC model, used only for its schema version.
:param entity_type: IFC class name, for example ``'IfcWall'``.
"""
schema_name = model.schema
try:
doc = ifcopenshell.util.doc.get_entity_doc(schema_name, entity_type)
+9 -1
View File
@@ -26,7 +26,15 @@ import ifcopenshell
def summary(model: ifcopenshell.file) -> dict[str, Any]:
"""Return a model overview with schema, element counts, and project info."""
"""Summarise the model: schema, entity counts and project info.
Returns the ``schema`` version, ``total_entities``, and a ``project``
block with the id, name and description of the first ``IfcProject``
(omitted if the model has none). The count covers every entity in the
file, not just physical elements.
:param model: The in-memory IFC model.
"""
# Count elements by IFC type, sorted by count descending
type_counter: Counter[str] = Counter()
total = 0
+11 -1
View File
@@ -59,7 +59,17 @@ def _build_spatial_node(element: ifcopenshell.entity_instance) -> dict[str, Any]
def tree(model: ifcopenshell.file) -> dict[str, Any] | list[dict[str, Any]]:
"""Return the spatial hierarchy tree starting from IfcProject."""
"""Return the spatial hierarchy of the model as a nested tree.
Starts at ``IfcProject`` and descends through decomposition (site,
building, storeys) and containment (the elements placed in each storey).
Every node carries ``id``, ``type`` and ``name``; ``children`` holds
decomposed sub-spaces and ``elements`` holds contained elements, and
either key is omitted when empty. Returns a list when the file contains
several projects, or an ``error`` key when it contains none.
:param model: The in-memory IFC model.
"""
projects = model.by_type("IfcProject")
if not projects:
return {"error": "No IfcProject found in model"}
+10 -1
View File
@@ -8,7 +8,16 @@ import ifcopenshell.validate
def validate(model: ifcopenshell.file, express_rules: bool = False) -> dict[str, Any]:
"""Validate the model and return a dict with 'valid' bool and 'issues' list."""
"""Validate the model against the IFC schema.
Returns ``valid`` together with a list of ``issues``, each carrying a
``level`` and a ``message``. Worth running after a batch of edits and
before writing the model back to disk.
:param model: The in-memory IFC model.
:param express_rules: Also evaluate the schema's EXPRESS rules. Catches
more problems but is considerably slower (default ``False``).
"""
logger = ifcopenshell.validate.json_logger()
ifcopenshell.validate.validate(model, logger, express_rules=express_rules)
issues = [{"level": s["level"], "message": s["message"]} for s in logger.statements]