mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-20 20:22:09 +00:00
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:
committed by
Thomas Krijnen
parent
e077390e3d
commit
6f3acc84ee
@@ -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)}
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user