Skip to content

solidworks_mcp.tools.error_classifier

solidworks_mcp.tools.error_classifier

Classify tool failures into actionable error categories.

The MCP tool error boundary (tool_boundary) asks this module what kind of failure it is looking at, so the payload can carry an error_type and a hint telling the caller what to do next instead of an opaque message.

Adapter errors usually arrive as strings - _handle_com_operation flattens COM exceptions to "COM error in <op>: (<hresult>, '<text>', ...)" - so every rule can match on the message text, and COM rules also on the HRESULT taken from either a com_error exception or that text.

Rules are evaluated in order; the first match wins. Add a category by appending a rule - callers never change.

Attributes

DEFAULT_RULES module-attribute

DEFAULT_RULES: tuple[ErrorRule, ...] = (ErrorRule(error_type='solidworks_unavailable', hint='SolidWorks is not reachable - it was closed or crashed. Restart SolidWorks (and the MCP server if it still fails), then retry.', hresults=frozenset({RPC_E_DISCONNECTED, RPC_S_SERVER_UNAVAILABLE, RPC_S_CALL_FAILED}), patterns=('rpc server is unavailable', 'object invoked has disconnected', 'not connected to solidworks', 'solidworks (application )?(is )?not connected', 'solidworks is not running')), ErrorRule(error_type='com_wrong_thread', hint='A SolidWorks call ran off the thread that connected - a server bug. Report the tool name; restart the MCP server to recover.', patterns=('belong to the thread that called connect',)), ErrorRule(error_type='circuit_open', hint='Calls to this operation are paused after repeated failures. Fix the underlying error, then call `reset_circuit_breaker` (or wait for the breaker timeout, SOLIDWORKS_MCP_CIRCUIT_BREAKER_TIMEOUT seconds) and retry.', patterns=('circuit breaker is (open|half[_-]open)',)), ErrorRule(error_type='com_member_not_found', hint='SolidWorks could not resolve this API member. Usually a server bug: a method called without being flagged as one (report the tool name). After a SolidWorks upgrade it can be a stale type cache: delete %TEMP%\\gen_py and restart the MCP server.', hresults=frozenset({DISP_E_MEMBERNOTFOUND}), patterns=('member not found',)), ErrorRule(error_type='com_type_mismatch', hint="SolidWorks rejected an argument's COM type. This is a server bug; report the tool name and arguments.", hresults=frozenset({DISP_E_TYPEMISMATCH}), patterns=('type mismatch',)), ErrorRule(error_type=NOT_SUPPORTED, hint=NOT_SUPPORTED_HINT, patterns=('does not support', 'no adapter capability', 'only available on windows', '\x08not available\x08')), ErrorRule(error_type='wrong_document_type', hint='The active document is the wrong kind for this tool. Create or activate the right one (create_part, create_assembly, create_drawing or activate_document), then retry.', patterns=('(requires|needs) an? (active )?(part|assembly|drawing)( or (part|assembly|drawing))? document',)), ErrorRule(error_type='no_active_sketch', hint='Create or open a sketch first (create_sketch), then retry.', patterns=('no active sketch',)), ErrorRule(error_type='no_active_document', hint='Open or create a document first (open_model / create_part), or activate one with activate_document, then retry.', patterns=('no active (model|document)',)), ErrorRule(error_type='feature_not_built', hint="SolidWorks accepted the call but the geometry gave it nothing to build. For a cut: the profile must be a closed loop that overlaps the solid within the depth; one-direction cuts already try both directions, so change the sketch, its plane, the depth or use end_condition 'ThroughAll' rather than retrying as is.", patterns=('built no feature',)), ErrorRule(error_type='not_found', hint='Check the name or path - list what exists first (list_features, list_open_documents, list_drawing_views) and use an exact name.', patterns=('\\bnot found\\b', 'does not exist', '\\bno such\\b', '\\bno [\\w.-]+( \\w+)? found\\b', '\\bhas no (\\w+ )?(edges|faces)\\b', '\\bhas no [\\w-]+ body\\b', '\\bno view named\\b')))

DISP_E_MEMBERNOTFOUND module-attribute

DISP_E_MEMBERNOTFOUND = -2147352573

DISP_E_TYPEMISMATCH module-attribute

DISP_E_TYPEMISMATCH = -2147352571

INTERNAL module-attribute

INTERNAL = 'internal'

INVALID_INPUT module-attribute

INVALID_INPUT = 'invalid_input'

NOT_SUPPORTED module-attribute

NOT_SUPPORTED = 'not_supported'

NOT_SUPPORTED_HINT module-attribute

NOT_SUPPORTED_HINT = 'The active adapter cannot perform this operation. Use the real SolidWorks adapter, another tool, or do this step in SolidWorks directly.'

OPERATION_FAILED module-attribute

OPERATION_FAILED = 'operation_failed'

RPC_E_DISCONNECTED module-attribute

RPC_E_DISCONNECTED = -2147417848

RPC_S_CALL_FAILED module-attribute

RPC_S_CALL_FAILED = -2147023170

RPC_S_SERVER_UNAVAILABLE module-attribute

RPC_S_SERVER_UNAVAILABLE = -2147023174

_FALLBACK_HINTS module-attribute

_FALLBACK_HINTS = {OPERATION_FAILED: 'The SolidWorks operation failed. Read the message for the cause, adjust the inputs, and retry.', INTERNAL: 'Unexpected server error - this is a bug, not a modelling problem. Report the tool name, arguments and this message.'}

_HRESULT_IN_TEXT module-attribute

_HRESULT_IN_TEXT = re.compile('\\((-\\d{9,10}),')

Classes

Classification dataclass

Classification(error_type: str, hint: str)

The category of a failure and the next step to suggest.

ErrorClassifier

ErrorClassifier(rules: tuple[ErrorRule, ...] = DEFAULT_RULES)

Map a failure to a :class:Classification using ordered rules.

Source code in src/solidworks_mcp/tools/error_classifier.py
def __init__(self, rules: tuple[ErrorRule, ...] = DEFAULT_RULES) -> None:
    self._rules = rules
Methods:
classify
classify(message: str, exc: BaseException | None = None, *, fallback: str = OPERATION_FAILED) -> Classification

Classify message (and exc when there is one).

Parameters:

Name Type Description Default
message str

The error text the caller will see.

required
exc BaseException | None

The exception, when the failure was raised rather than returned.

None
fallback str

Category for an unrecognised failure: operation_failed for adapter errors, internal for unexpected exceptions.

OPERATION_FAILED

Returns:

Name Type Description
Classification Classification

The first matching rule, else the fallback.

Source code in src/solidworks_mcp/tools/error_classifier.py
def classify(
    self,
    message: str,
    exc: BaseException | None = None,
    *,
    fallback: str = OPERATION_FAILED,
) -> Classification:
    """Classify ``message`` (and ``exc`` when there is one).

    Args:
        message: The error text the caller will see.
        exc: The exception, when the failure was raised rather than returned.
        fallback: Category for an unrecognised failure: ``operation_failed``
            for adapter errors, ``internal`` for unexpected exceptions.

    Returns:
        Classification: The first matching rule, else the fallback.
    """
    hresult = extract_hresult(message, exc)
    for rule in self._rules:
        if rule.matches(message, hresult):
            return Classification(rule.error_type, rule.hint)
    return Classification(fallback, _FALLBACK_HINTS[fallback])

ErrorRule dataclass

ErrorRule(error_type: str, hint: str, hresults: frozenset[int] = frozenset(), patterns: tuple[str, ...] = ())

Matches a failure by HRESULT and/or a case-insensitive message pattern.

Methods:
matches
matches(message: str, hresult: int | None) -> bool

Return True when the HRESULT or any pattern matches.

Source code in src/solidworks_mcp/tools/error_classifier.py
def matches(self, message: str, hresult: int | None) -> bool:
    """Return True when the HRESULT or any pattern matches."""
    if hresult is not None and hresult in self.hresults:
        return True
    return any(re.search(p, message, re.IGNORECASE) for p in self.patterns)

Functions:

extract_hresult

extract_hresult(message: str, exc: BaseException | None = None) -> int | None

Return the COM HRESULT from a com_error or from flattened text.

Source code in src/solidworks_mcp/tools/error_classifier.py
def extract_hresult(message: str, exc: BaseException | None = None) -> int | None:
    """Return the COM HRESULT from a ``com_error`` or from flattened text."""
    if exc is not None:
        args = getattr(exc, "args", ())
        if args and isinstance(args[0], int):
            return args[0]
    match = _HRESULT_IN_TEXT.search(message)
    return int(match.group(1)) if match else None