Skip to main content
Glama
pzfreo

build123d-mcp

validate_code

Check build123d code for syntax errors, blocked imports, and missing calls to prevent failed execution.

Instructions

Check build123d code for syntax errors, blocked imports/calls, and common omissions before executing. Returns {syntax, blocked, warnings, ok}. blocked items prevent execution; warnings are advisory (e.g. no build123d import in this snippet, no result/show() call). Use this before a long generated script to catch obvious problems without burning a session execute().

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
codeYes

Output Schema

TableJSON Schema
NameRequiredDescriptionDefault
resultYes

Implementation Reference

  • Core handler that validates build123d code using AST inspection: checks syntax, blocked imports (against IMPORT_ALLOWLIST), blocked calls (against _BLOCKED_CALL_NAMES), dunder attribute access, and advisory warnings for missing build123d import or no 'result' assignment/show() call. Returns JSON string with 'syntax', 'blocked', 'warnings', and 'ok' fields.
    def validate_code(code: str) -> str:
        # Syntax check
        try:
            tree = ast.parse(code)
        except SyntaxError as e:
            return json.dumps({
                "syntax": f"SyntaxError at line {e.lineno}: {e.msg}",
                "blocked": [],
                "warnings": [],
                "ok": False,
            }, indent=2)
    
        blocked = []
        warnings = []
    
        # Security: blocked imports and calls
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    root = alias.name.split(".")[0]
                    if root not in IMPORT_ALLOWLIST:
                        blocked.append(f"import '{alias.name}' is not allowed")
            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    root = node.module.split(".")[0]
                    if root not in IMPORT_ALLOWLIST:
                        blocked.append(f"import '{node.module}' is not allowed")
            elif isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name) and node.func.id in _BLOCKED_CALL_NAMES:
                    blocked.append(f"call to '{node.func.id}' is not allowed")
            elif isinstance(node, ast.Attribute):
                if node.attr.startswith("__") and node.attr.endswith("__"):
                    blocked.append(f"dunder attribute access '{node.attr}' is not allowed")
    
        # Advisory: no build123d import in this snippet
        has_b3d_import = any(
            (isinstance(n, ast.ImportFrom) and n.module and n.module.startswith("build123d"))
            or (isinstance(n, ast.Import) and any(a.name.startswith("build123d") for a in n.names))
            for n in ast.walk(tree)
        )
        if not has_b3d_import:
            warnings.append("no build123d import in this snippet — ok if already imported in a prior execute()")
    
        # Advisory: no result variable or show() call
        has_output = any(
            (isinstance(n, ast.Assign) and any(
                isinstance(t, ast.Name) and t.id == "result" for t in n.targets
            ))
            or (isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == "show")
            for n in ast.walk(tree)
        )
        if not has_output:
            warnings.append("no 'result' assignment or show() call — the session won't capture a shape unless current_shape is set by other means")
    
        return json.dumps({
            "syntax": "ok",
            "blocked": blocked,
            "warnings": warnings,
            "ok": len(blocked) == 0,
        }, indent=2)
  • Return JSON schema for validate_code output: fields 'syntax' (string), 'blocked' (list of strings), 'warnings' (list of strings), 'ok' (boolean).
    return json.dumps({
        "syntax": "ok",
        "blocked": blocked,
        "warnings": warnings,
        "ok": len(blocked) == 0,
    }, indent=2)
  • MCP tool registration for 'validate_code' using @mcp.tool() decorator. The server function delegates to the handler in tools/validate_code.py.
    @mcp.tool()
    def validate_code(code: str) -> str:
        """Check build123d code for syntax errors, blocked imports/calls, and common omissions before executing. Returns {syntax, blocked, warnings, ok}. blocked items prevent execution; warnings are advisory (e.g. no build123d import in this snippet, no result/show() call). Use this before a long generated script to catch obvious problems without burning a session execute()."""
        from build123d_mcp.tools.validate_code import validate_code as _validate_code
        return _validate_code(code)
  • IMPORT_ALLOWLIST define which module roots are permitted for imports (build123d, math, numpy, json, etc.), used by validate_code's AST checking logic.
    IMPORT_ALLOWLIST = frozenset({
        # CAD libraries
        "build123d",
        "bd_warehouse",
        # Numeric / math
        "math",
        "numpy",
        "decimal",
        "fractions",
        "statistics",
        "numbers",
        "random",
        # Data structures / utilities
        "collections",
        "itertools",
        "functools",
        "copy",
        "operator",
        "struct",
        # Type system
        "typing",
        "abc",
        "dataclasses",
        "enum",
        # String / text
        "re",
        "string",
        "textwrap",
        "pprint",
        # Serialisation (in-memory only — no I/O)
        "json",
        "base64",
        "hashlib",
        # Misc stdlib
        "io",
        "warnings",
        "contextlib",
    })
    
    # When True, import checks are skipped entirely.  Set via --allow-all-imports.
    ALLOW_ALL_IMPORTS: bool = False
    
    # Builtins that are dangerous even without an import.
    _BLOCKED_BUILTINS = frozenset({
        "eval", "exec", "compile", "open", "breakpoint", "input",
        # Introspection builtins that enable subclass-traversal sandbox escapes.
        "getattr", "vars", "dir", "hasattr",
    })
    
    # Bare-name calls that are caught at the AST level (before exec runs).
    _BLOCKED_CALL_NAMES = frozenset({
        "__import__", "eval", "exec", "compile", "open", "breakpoint", "input",
        # Introspection calls that can bypass dunder-attribute blocking via strings.
        "getattr", "vars", "dir", "hasattr",
    })
    
    
    # ---------------------------------------------------------------------------
    # Layer 1: AST inspection
    # ---------------------------------------------------------------------------
    
    def check_ast(code: str) -> None:
        """Raise ValueError if code contains disallowed imports or dangerous calls.
    
        Catches the most common injection patterns before exec() is ever called.
        Syntax errors are left for exec() to report with better messages.
        """
        try:
            tree = ast.parse(code)
        except SyntaxError:
            return
    
        if ALLOW_ALL_IMPORTS:
            # Still block dangerous calls even in unrestricted mode.
            for node in ast.walk(tree):
                if isinstance(node, ast.Call):
                    if isinstance(node.func, ast.Name) and node.func.id in _BLOCKED_CALL_NAMES:
                        raise ValueError(f"Call to '{node.func.id}' is not allowed.")
            return
    
        for node in ast.walk(tree):
            if isinstance(node, ast.Import):
                for alias in node.names:
                    _check_module(alias.name)
            elif isinstance(node, ast.ImportFrom):
                if node.module:
                    _check_module(node.module)
            elif isinstance(node, ast.Call):
                if isinstance(node.func, ast.Name) and node.func.id in _BLOCKED_CALL_NAMES:
                    raise ValueError(
                        f"Call to '{node.func.id}' is not allowed."
                    )
            elif isinstance(node, ast.Attribute):
                if node.attr.startswith("__") and node.attr.endswith("__"):
                    raise ValueError(
                        f"Access to dunder attribute '{node.attr}' is not allowed. "
                        f"Use operators and language syntax instead of explicit dunder access."
                    )
    
    
    def _check_module(dotted_name: str) -> None:
        root = dotted_name.split(".")[0]
        if root not in IMPORT_ALLOWLIST:
            raise ValueError(
                f"Import of '{dotted_name}' is not allowed. "
                f"This blocks filesystem (os, pathlib, shutil), network (socket, urllib, "
                f"requests), and shell access (subprocess). "
                f"Permitted: {sorted(IMPORT_ALLOWLIST)}"
            )
    
    
    # ---------------------------------------------------------------------------
    # Layer 2: Restricted builtins
    # ---------------------------------------------------------------------------
    
    def make_restricted_builtins() -> dict[str, Any]:
        """Return a __builtins__ dict with dangerous functions removed.
    
        open / eval / exec / compile are removed outright.
        __import__ is replaced with an allowlisted version so that
        'from build123d import *' works but 'import os' is blocked at the
        namespace level even if AST inspection is somehow bypassed.
        """
        import builtins
        safe = vars(builtins).copy()
    
        for name in _BLOCKED_BUILTINS:
            safe.pop(name, None)
    
        _original_import = safe["__import__"]
    
        if ALLOW_ALL_IMPORTS:
            safe["__import__"] = _original_import
            return safe
    
        def _safe_import(name: str, *args: Any, **kwargs: Any) -> Any:
            root = name.split(".")[0]
            if root not in IMPORT_ALLOWLIST:
                raise ImportError(
                    f"Import of '{name}' is not allowed. "
                    f"Permitted: {sorted(IMPORT_ALLOWLIST)}"
                )
            return _original_import(name, *args, **kwargs)
    
        safe["__import__"] = _safe_import
        return safe
    
    
    # ---------------------------------------------------------------------------
    # Timeout exception (raised by SIGALRM in Session or propagated by WorkerSession)
    # ---------------------------------------------------------------------------
    
    class ExecutionTimeout(Exception):
        pass
  • _BLOCKED_CALL_NAMES list of bare-name function calls blocked at AST level (eval, exec, open, getattr, etc.), used by validate_code's security checks.
    _BLOCKED_CALL_NAMES = frozenset({
        "__import__", "eval", "exec", "compile", "open", "breakpoint", "input",
        # Introspection calls that can bypass dunder-attribute blocking via strings.
        "getattr", "vars", "dir", "hasattr",
    })
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations, so description carries full burden. It describes return structure (syntax, blocked, warnings, ok) and explains that blocked items prevent execution while warnings are advisory. Missing details on side effects but validation is likely side-effect-free.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, front-loaded with purpose and return structure. No wasted words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given output schema documents return fields, description explains them well. Only one parameter. Could mention that validation is local static analysis, but completeness is adequate.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Single parameter 'code' with 0% schema description coverage. The description only implies it is a string of build123d code but does not add details about format, required packages, or expected length. More elaboration would be beneficial.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it checks code for syntax errors, blocked imports/calls, and common omissions, distinguishing it from sibling 'execute' which runs code. The verb 'validate' plus resource 'code' is specific.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly advises using this before a long generated script to catch problems without burning a session execute(). However, it does not mention alternative tools like workflow_hints.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/pzfreo/build123d-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server