mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-15 18:14:08 +00:00
6f3acc84ee
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.
238 lines
8.2 KiB
Python
238 lines
8.2 KiB
Python
# This file was generated with the assistance of an AI coding tool.
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import inspect
|
|
from typing import Any
|
|
|
|
from ifcmcp.core import IfcSession
|
|
|
|
try:
|
|
from mcp.server.fastmcp import FastMCP # type: ignore
|
|
from mcp.types import ImageContent # type: ignore
|
|
except ImportError: # pragma: no cover
|
|
FastMCP = None # type: ignore
|
|
ImageContent = None # type: ignore
|
|
|
|
|
|
def build_server() -> Any:
|
|
"""Create the FastMCP server if the dependency is available."""
|
|
if FastMCP is None:
|
|
raise ImportError(
|
|
"FastMCP is not installed. Install with: pip install ifcmcp[mcp] " "(or add 'mcp' to your environment)."
|
|
)
|
|
|
|
session = IfcSession()
|
|
|
|
def _tool(fn):
|
|
"""Register a tool, taking its MCP description from the identically-named
|
|
IfcSession method rather than duplicating it here."""
|
|
return server.tool(description=inspect.getdoc(getattr(IfcSession, fn.__name__)))(fn)
|
|
|
|
server = FastMCP(
|
|
name="ifc-mcp",
|
|
instructions=(
|
|
"MCP server for querying and editing IFC building models. "
|
|
"Load a file first with ifc_load, then use query/edit tools. "
|
|
"Save changes with ifc_save."
|
|
),
|
|
)
|
|
|
|
# ---- Lifecycle ----
|
|
@_tool
|
|
def ifc_new(schema: str = "IFC4") -> dict[str, Any]:
|
|
return session.ifc_new(schema=schema)
|
|
|
|
@_tool
|
|
def ifc_load(path: str) -> str:
|
|
return session.ifc_load(path)
|
|
|
|
@_tool
|
|
def ifc_save(path: str = "") -> str:
|
|
return session.ifc_save(path)
|
|
|
|
@_tool
|
|
def ifc_reset() -> dict[str, Any]:
|
|
return session.ifc_reset()
|
|
|
|
# ---- Query ----
|
|
@_tool
|
|
def ifc_summary() -> dict[str, Any]:
|
|
return session.ifc_summary()
|
|
|
|
@_tool
|
|
def ifc_tree() -> dict[str, Any] | list[dict[str, Any]]:
|
|
return session.ifc_tree()
|
|
|
|
@_tool
|
|
def ifc_info(element_id: int) -> dict[str, Any]:
|
|
return session.ifc_info(element_id)
|
|
|
|
@_tool
|
|
def ifc_select(query: str) -> list[dict[str, Any]]:
|
|
return session.ifc_select(query)
|
|
|
|
@_tool
|
|
def ifc_relations(element_id: int, traverse: str = "") -> dict[str, Any] | list[dict[str, Any]]:
|
|
return session.ifc_relations(element_id, traverse=traverse)
|
|
|
|
@_tool
|
|
def ifc_clash(
|
|
element_id: int,
|
|
clearance: float = 0.0,
|
|
tolerance: float = 0.002,
|
|
scope: str = "storey",
|
|
) -> dict[str, Any]:
|
|
return session.ifc_clash(
|
|
element_id=element_id,
|
|
clearance=clearance,
|
|
tolerance=tolerance,
|
|
scope=scope,
|
|
)
|
|
|
|
@_tool
|
|
def ifc_contexts() -> list[dict[str, Any]]:
|
|
return session.ifc_contexts()
|
|
|
|
@_tool
|
|
def ifc_materials() -> list[dict[str, Any]]:
|
|
return session.ifc_materials()
|
|
|
|
# ---- Edit ----
|
|
@_tool
|
|
def ifc_list(module: str = "") -> list[dict]:
|
|
return session.ifc_list(module=module)
|
|
|
|
@_tool
|
|
def ifc_docs(function_path: str) -> dict:
|
|
return session.ifc_docs(function_path=function_path)
|
|
|
|
@_tool
|
|
def ifc_edit(function_path: str, params: str = "{}") -> dict:
|
|
return session.ifc_edit(function_path=function_path, params=params)
|
|
|
|
# ---- Extended query + edit ----
|
|
@_tool
|
|
def ifc_validate(express_rules: bool = False) -> dict[str, Any]:
|
|
return session.ifc_validate(express_rules=express_rules)
|
|
|
|
@_tool
|
|
def ifc_schedule(max_depth: int | None = None) -> list[dict[str, Any]]:
|
|
return session.ifc_schedule(max_depth=max_depth)
|
|
|
|
@_tool
|
|
def ifc_cost(max_depth: int | None = None) -> list[dict[str, Any]]:
|
|
return session.ifc_cost(max_depth=max_depth)
|
|
|
|
@_tool
|
|
def ifc_schema(entity_type: str) -> dict[str, Any]:
|
|
return session.ifc_schema(entity_type=entity_type)
|
|
|
|
@_tool
|
|
def ifc_quantify(rule: str, selector: str = "") -> dict[str, Any]:
|
|
return session.ifc_quantify(rule=rule, selector=selector)
|
|
|
|
# ---- Shape builder ----
|
|
@_tool
|
|
def ifc_shape_list() -> list[dict]:
|
|
return session.ifc_shape_list()
|
|
|
|
@_tool
|
|
def ifc_shape_docs(method: str) -> dict:
|
|
return session.ifc_shape_docs(method=method)
|
|
|
|
@_tool
|
|
def ifc_shape(method: str, params: str = "{}") -> dict:
|
|
return session.ifc_shape(method=method, params=params)
|
|
|
|
@server.tool(structured_output=False)
|
|
def ifc_plot(
|
|
selector: str = "",
|
|
element_ids: list[int] | None = None,
|
|
view: str = "floorplan",
|
|
width_mm: float = 297.0,
|
|
height_mm: float = 420.0,
|
|
scale: float = 1.0 / 100.0,
|
|
png_width: int = 1024,
|
|
png_height: int = 1024,
|
|
output_path: str = "",
|
|
) -> list[ImageContent]:
|
|
"""Generate a 2D technical drawing of the loaded IFC model.
|
|
|
|
Returns an inline PNG image (floor plan, elevation, or section) that the
|
|
LLM can inspect to understand the 2D layout of the model. If
|
|
``output_path`` is provided the drawing is also saved to disk — as SVG
|
|
when the path ends in ``.svg``, otherwise as PNG.
|
|
|
|
:param selector: ifcopenshell selector to restrict plotted elements
|
|
(e.g. ``'IfcWall'``). Omit to plot the whole model.
|
|
:param element_ids: Step IDs of elements to highlight. Other elements
|
|
are faded so the subject stands out.
|
|
:param view: Drawing view — ``floorplan`` (default), ``elevation``,
|
|
``section``, or ``auto``.
|
|
:param width_mm: Paper width in mm (default 297 = A4 landscape width).
|
|
:param height_mm: Paper height in mm (default 420 = A4 landscape height).
|
|
:param scale: Model-to-paper scale ratio (default 0.01 = 1:100).
|
|
:param png_width: Raster output width in pixels (default 1024).
|
|
:param png_height: Raster output height in pixels (default 1024).
|
|
:param output_path: Optional file path to save the drawing to disk.
|
|
"""
|
|
png_bytes = session.ifc_plot(
|
|
selector=selector,
|
|
element_ids=element_ids,
|
|
view=view,
|
|
width_mm=width_mm,
|
|
height_mm=height_mm,
|
|
scale=scale,
|
|
png_width=png_width,
|
|
png_height=png_height,
|
|
output_format="png",
|
|
)
|
|
if output_path:
|
|
if output_path.endswith(".svg"):
|
|
svg_bytes = session.ifc_plot(
|
|
selector=selector,
|
|
element_ids=element_ids,
|
|
view=view,
|
|
width_mm=width_mm,
|
|
height_mm=height_mm,
|
|
scale=scale,
|
|
output_format="svg",
|
|
)
|
|
with open(output_path, "wb") as f:
|
|
f.write(svg_bytes)
|
|
else:
|
|
with open(output_path, "wb") as f:
|
|
f.write(png_bytes)
|
|
return [ImageContent(type="image", data=base64.b64encode(png_bytes).decode(), mimeType="image/png")]
|
|
|
|
@server.tool(structured_output=False)
|
|
def ifc_render(
|
|
selector: str = "",
|
|
element_ids: list[int] | None = None,
|
|
view: str = "iso",
|
|
output_path: str = "",
|
|
) -> list[ImageContent]:
|
|
"""Render the loaded IFC model to a PNG image.
|
|
|
|
Returns an inline image the LLM can inspect to understand the spatial
|
|
layout of the model or a specific element in context. If
|
|
``output_path`` is provided the PNG is also saved to that file path.
|
|
|
|
:param selector: ifcopenshell selector to restrict rendered elements
|
|
(e.g. ``'IfcWall'``, ``'IfcBuildingStorey[Name="0"]'``).
|
|
Omit to render the whole model.
|
|
:param element_ids: Step IDs of elements to highlight. Other elements
|
|
are rendered in translucent grey so the subject stands out.
|
|
:param view: Camera angle — ``iso`` (default), ``top``, ``south``,
|
|
``north``, ``east``, or ``west``.
|
|
:param output_path: Optional file path to save the PNG to disk.
|
|
"""
|
|
png_bytes = session.ifc_render(selector=selector, element_ids=element_ids, view=view)
|
|
if output_path:
|
|
with open(output_path, "wb") as f:
|
|
f.write(png_bytes)
|
|
return [ImageContent(type="image", data=base64.b64encode(png_bytes).decode(), mimeType="image/png")]
|
|
|
|
return server
|