twincat-validator-mcp
Deterministic validation and auto-fix for LLM-generated TwinCAT 3 PLC code
The server acts as a deterministic safety net for LLM-generated TwinCAT code β the LLM writes the PLC logic, and the server guarantees it will import and compile in TwinCAT XAE. It connects to any MCP-compatible client (Claude Desktop, Cursor, VS Code, Windsurf, Cline) via stdio.
WHAT IT DOES
Six deterministic capabilities that no amount of prompt engineering can reliably replace.
Validates
TwinCAT XML files (.TcPOU, .TcIO, .TcDUT, .TcGVL) against 34+ structural, style, and OOP checks.
Auto-Fixes
9 categories of common issues in a dependency-aware, idempotent pipeline. Run twice β byte-identical output.
Scaffolds
Canonical XML skeletons for new file types from generation contracts. No hand-written boilerplate.
Orchestrates
Full validate β fix β re-validate β determinism-verify pipelines with loop prevention built in.
Enforces OOP
IEC 61131-3 OOP contracts β 21 checks β with configurable per-project policy via .twincat-validator.json.
Provides Knowledge
LLM-friendly knowledge base, generation contracts, and 8 reusable prompt templates baked into the server.
SUPPORTED FILE TYPES
All four Beckhoff TwinCAT 3 XML formats β the server understands full XML schema, CDATA-embedded ST declarations, method/property XML elements, GUID structures, and LineIds metadata.
LAYERED ARCHITECTURE
Six clean layers β each with a single responsibility. The LLM client only ever touches the MCP Interface Layer. Data flows strictly top-down; no layer skips.
Layer stack β left to right, outermost to innermost
graph LR
CLIENT["LLM Client"]
MCP["MCP Interface"]
ENGINE["Engine Layer"]
CHECKFIX["Check / Fix Layer"]
DOMAIN["Domain Layer"]
CONFIG["Config Layer"]
CLIENT -->|"stdio JSON-RPC"| MCP
MCP --> ENGINE
ENGINE --> CHECKFIX
CHECKFIX --> DOMAIN
DOMAIN --> CONFIG
style CLIENT fill:#1a1a1e,stroke:#04d9ff,color:#e2e8f0
style MCP fill:#1a1a1e,stroke:#9e4aff,color:#e2e8f0
style ENGINE fill:#1a1a1e,stroke:#fec20b,color:#e2e8f0
style CHECKFIX fill:#1a1a1e,stroke:#ff4fd8,color:#e2e8f0
style DOMAIN fill:#1a1a1e,stroke:#00ff7f,color:#e2e8f0
style CONFIG fill:#1a1a1e,stroke:#1f75ff,color:#e2e8f0 Each arrow represents a strict dependency boundary. The MCP Interface Layer is the only public API β all LLM tool calls, resource reads, and prompt fetches go through it. The Domain Layer owns the TwinCATFile value object and never writes to disk.
DATA FLOW
Every tool call follows the same two-phase pattern: a request phase that loads and processes the file, and a response phase that assembles the result envelope.
graph LR
PROMPT["User Prompt"] --> LLM["LLM Client"]
LLM -->|"Tool Call"| HANDLER["Tool Handler"]
HANDLER --> FILE["TwinCATFile"]
HANDLER --> VENGINE["ValidationEngine"]
HANDLER --> FENGINE["FixEngine"]
FILE --> VENGINE
FILE --> FENGINE
style PROMPT fill:#1a1a1e,stroke:#04d9ff,color:#e2e8f0
style LLM fill:#1a1a1e,stroke:#04d9ff,color:#e2e8f0
style HANDLER fill:#1a1a1e,stroke:#9e4aff,color:#e2e8f0
style FILE fill:#1a1a1e,stroke:#00ff7f,color:#e2e8f0
style VENGINE fill:#1a1a1e,stroke:#ff4fd8,color:#e2e8f0
style FENGINE fill:#1a1a1e,stroke:#ff4fd8,color:#e2e8f0 The Tool Handler normalizes inputs and dispatches to one or both engines simultaneously. TwinCATFile is the shared value object β both engines read from it, never from disk.
graph LR
VENGINE["ValidationEngine"] --> CONTRACT["ContractState"]
FENGINE["FixEngine"] --> CONTRACT
CONTRACT --> ENVELOPE["Response Envelope"]
ENVELOPE --> LLM["LLM Client"]
LLM --> USER["User"]
style VENGINE fill:#1a1a1e,stroke:#ff4fd8,color:#e2e8f0
style FENGINE fill:#1a1a1e,stroke:#ff4fd8,color:#e2e8f0
style CONTRACT fill:#1a1a1e,stroke:#00ff7f,color:#e2e8f0
style ENVELOPE fill:#1a1a1e,stroke:#9e4aff,color:#e2e8f0
style LLM fill:#1a1a1e,stroke:#04d9ff,color:#e2e8f0
style USER fill:#1a1a1e,stroke:#04d9ff,color:#e2e8f0 ContractState is the single source of truth for safe_to_import and safe_to_compile β computed once, used by every tool. The Response Envelope wraps it with policy proof, timing, and server version metadata.
MCP INTERFACE
16 tools Β· 11 resources Β· 8 prompt templates β the complete surface area exposed to any LLM client.
mcp_tools_validation.py 5 tools
| Tool | Purpose |
|---|---|
| validate_file | Full validation of one file. Supports all/critical/style levels and full/llm_strict output profiles. |
| validate_for_import | Quick critical-only gate β returns safe_to_import boolean with any blocking issues. |
| check_specific | Run a named subset of checks (e.g., only guid_format and naming_conventions). |
| get_validation_summary | 0β100 health score with issue breakdown and estimated fix time. |
| suggest_fixes | Takes a validation result JSON and generates prioritized fix recommendations. |
mcp_tools_fix.py 3 tools
| Tool | Purpose |
|---|---|
| autofix_file | Applies all safe fixes in deterministic order. Supports canonical formatting, strict contract enforcement, and implicit file creation. |
| generate_skeleton | Emits a canonical XML skeleton for a given file type and POU subtype from generation_contract.json templates. |
| extract_methods_to_xml | Promotes inline METHOD...END_METHOD blocks from ST code into proper <Method> XML elements. |
mcp_tools_orchestration.py 6 tools
| Tool | Purpose |
|---|---|
| process_twincat_single | RECOMMENDED: Full enforced pipeline β validate β autofix β re-validate β suggest fixes if still unsafe. |
| process_twincat_batch | Full pipeline across multiple files with summary or full response modes. |
| verify_determinism_batch | Runs the strict pipeline twice and reports per-file idempotence stability. |
| get_effective_oop_policy | Resolves the active OOP validation policy by walking ancestor directories for .twincat-validator.json. |
| lint_oop_policy | Validates the config file itself β checks key names, types, value ranges. |
| get_context_pack | Returns curated knowledge-base entries scoped to pre_generation or troubleshooting workflow stage. |
mcp_resources.py 11 read-only endpoints
prompts.py 8 templates
CORE DESIGN PATTERNS
Nine engineering patterns that make the server deterministic, reliable, and LLM-friendly.
6.1 Registry Pattern (Auto-Discovery)
Decorator-based registry auto-discovers all checks and fixes at import time β no manual registration lists.
Both validators and fixers use @CheckRegistry.register and @FixRegistry.register decorators. The __init__.py modules use pkgutil.iter_modules() to auto-import all *_checks.py and *_fixes.py at package load time, triggering registration. Adding a new check requires only: (1) create the class, (2) add config entry.
@CheckRegistry.register
class GuidFormatCheck(BaseCheck):
check_id = "guid_format"
def run(self, file: TwinCATFile) -> list[ValidationIssue]: ...
@FixRegistry.register
class TabsFix(BaseFix):
fix_id = "tabs"
def apply(self, file: TwinCATFile) -> bool: ... 6.2 Configuration-Driven Architecture
All rules, fixes, naming conventions, knowledge base and generation contracts are in JSON β not hard-coded.
validation_rules.json (34 checks), fix_capabilities.json (9 fixes), naming_conventions.json, knowledge_base.json (LLM-friendly explanations), generation_contract.json (canonical XML templates). Non-developers can review and modify rules without touching Python. LLM clients can read config via MCP resources.
6.3 TwinCATFile Value Object
Content is always authoritative β XML parsing uses in-memory content, never disk. Cache invalidates on mutation.
Setting file.content = new_content automatically clears _lines and _xml_tree caches while preserving _pou_subtype cache (POU type does not change during fixes). Factory method TwinCATFile.from_path() validates file existence and extension before construction.
6.4 Dual-Profile Response System
full profile for human debugging, llm_strict profile for machine consumption (minimal tokens, boolean signals).
full: verbose with all checks, issues with explanations, code snippets, fix suggestions, timing. llm_strict: only safe_to_import, safe_to_compile, blocking_count, blockers. The LLM uses llm_strict in automated pipelines and full when the user asks "what's wrong with this file?"
6.5 Contract State (Single Source of Truth)
ContractState is the ONLY place that computes safe_to_import and safe_to_compile β every tool calls derive_contract_state().
Eliminates inconsistencies between tools. safe_to_import = (error_count == 0). safe_to_compile = (error_count == 0). blocking_count = count(unfixable issues where severity in [error, critical]). Warnings do NOT block.
safe_to_import = (error_count == 0) safe_to_compile = (error_count == 0) # warnings do NOT block blocking_count = count(unfixable issues where severity in ["error", "critical"]) done = safe_to_import and safe_to_compile and blocking_count == 0 status = "done" if done else "blocked"
6.6 Policy-Enforcement Context
Every OOP-sensitive tool call resolves an ExecutionContext β the policy proof travels with every response.
Context contains: target path, policy source (defaults vs. project override file), effective OOP policy (merged), policy fingerprint (SHA-256 of canonical JSON), enforcement mode (strict or compat). The LLM and user can verify which policy was active and whether it was resolved successfully.
6.7 Intent-Aware OOP Routing
intent_profile parameter routes validation: auto (scan for EXTENDS/IMPLEMENTS), procedural (skip OOP), oop (always run OOP).
For batch operations, _batch_auto_resolve_intent() scans all .TcPOU files β if ANY file uses OOP keywords, all files are validated with OOP checks enabled. Prevents false negatives in mixed codebases.
6.8 Deterministic Fix Ordering
Fixes execute in a strict dependency-aware order defined by the order field in fix_capabilities.json.
Order: (1) tabsβspaces, (2) file ending, (3) property newlines, (4) CDATA formatting, (5) property VAR blocks, (6) excessive blank lines, (7) indentation, (8) GUID case, (9) LineIds. Idempotency guarantee: autofix(autofix(file)) == autofix(file).
6.9 Orchestration Loop with Loop Prevention
Bounded remediation loop (max 3 iterations) with content fingerprinting to detect no-progress situations.
orchestration_hints=True adds: no_change_detected, content_fingerprint_before/after (SHA-256), issue_fingerprint, no_progress_count, next_action, terminal flag. Stop conditions: max 3 iterations, stop if no_change_detected and file remains unsafe, stop if issue_fingerprint repeats with no_progress_count >= 2.
VALIDATION CHECK CATALOG
34 checks across three severity categories. Critical checks block import; style checks are advisory.
Structure & Format Checks (Critical β Blocks Import)
| Check ID | What It Detects |
|---|---|
| xml_structure | Invalid or malformed XML |
| guid_format | GUID does not match expected format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
| guid_uniqueness | Duplicate GUIDs within a file |
| file_ending | File does not end with </TcPlcObject> properly |
| property_var_blocks | Property getters missing VAR/END_VAR blocks |
| lineids_count | LineIds count does not match method/property count |
| pou_structure | POU structural violations (VAR PROTECTED, etc.) |
Style Checks (Warning β Advisory)
| Check ID | What It Detects |
|---|---|
| tabs | Tab characters (TwinCAT requires spaces) |
| indentation | Non-2-space indentation |
| element_ordering | XML elements in wrong order |
| naming_conventions | Missing FB_, PRG_, FUNC_, E_, ST_, I_, GVL_ prefixes |
| excessive_blank_lines | More than 2 consecutive blank lines |
| cdata_formatting | CDATA section formatting issues |
OOP Checks β IEC 61131-3 (21 Checks, run when EXTENDS/IMPLEMENTS detected)
AUTO-FIX PIPELINE
9 fix operations executed in a strict dependency-aware order defined by fix_capabilities.json. The order field ensures fixes never conflict with each other.
Running autofix(autofix(file)) produces byte-identical output to autofix(file). Fixes never fight each other β tabs are converted before indentation is checked, and the order field in fix_capabilities.json enforces these dependencies statically.
LLM WORKFLOW INTEGRATION
The recommended end-to-end flow has three distinct phases: setup, correction loop, and determinism verification. Each phase is a separate concern.
graph LR
PROMPT["User Prompt"] --> CTX["get_context_pack"]
CTX --> SKEL["generate_skeleton"]
SKEL --> WRITE["LLM writes ST"]
WRITE --> PROC["process_twincat"]
style PROMPT fill:#1a1a1e,stroke:#04d9ff,color:#e2e8f0
style CTX fill:#1a1a1e,stroke:#9e4aff,color:#e2e8f0
style SKEL fill:#1a1a1e,stroke:#9e4aff,color:#e2e8f0
style WRITE fill:#1a1a1e,stroke:#04d9ff,color:#e2e8f0
style PROC fill:#1a1a1e,stroke:#00ff7f,color:#e2e8f0 Before writing any code, the LLM calls get_context_pack(stage="pre_generation") to load relevant knowledge-base entries and OOP policy. Then generate_skeleton produces a canonical XML skeleton β the LLM fills in the ST logic, never starts from a blank file.
graph LR
PROC["process_twincat"] --> SAFE["Safe to import?"]
SAFE -->|Yes| VERIFY["verify_determinism"]
SAFE -->|No| FIX["LLM correction"]
FIX -->|"next pass"| BLOCKED["Blocked / done"]
style PROC fill:#1a1a1e,stroke:#00ff7f,color:#e2e8f0
style SAFE fill:#fec20b,stroke:#fec20b,color:#0d0d0f
style VERIFY fill:#1a1a1e,stroke:#fec20b,color:#e2e8f0
style FIX fill:#1a1a1e,stroke:#04d9ff,color:#e2e8f0
style BLOCKED fill:#ff4fd8,stroke:#ff4fd8,color:#0d0d0f process_twincat_single runs the full validate β autofix β re-validate pipeline in one call. If the file is still unsafe, the LLM gets a focused troubleshooting context pack and applies one targeted correction, then loops back. Hard stop at 3 iterations or when no progress is detected (fingerprint-based).
graph LR
VERIFY["verify_determinism"] --> HASH["SHA-256 compare"]
HASH -->|"Match"| DONE["Done β"]
HASH -->|"Mismatch"| RETRY["Re-process"]
style VERIFY fill:#1a1a1e,stroke:#fec20b,color:#e2e8f0
style HASH fill:#fec20b,stroke:#fec20b,color:#0d0d0f
style DONE fill:#00ff7f,stroke:#00ff7f,color:#0d0d0f
style RETRY fill:#1a1a1e,stroke:#00ff7f,color:#e2e8f0 verify_determinism_batch runs the full pipeline a second time and compares content fingerprints (SHA-256). If the output changes on the second pass, the file is not stable and re-enters the loop. A file is only "Done" when it is safe to import and produces byte-identical output across two runs.
HEALTH SCORE SYSTEM
Every file gets a 0β100 score. Warnings don't block import β only errors do.
Score Deductions
Score Ratings
TECH STACK & METRICS
Zero external dependencies beyond the MCP SDK. The entire validator uses only Python standard library.
XML parsing (xml.etree.ElementTree), regex (re), hashing (hashlib SHA-256), and data models (dataclasses) β all Python stdlib. The only runtime dependency is mcp>=1.0.0. This means no dependency conflicts, no security surface beyond the MCP SDK, no version pinning nightmares.
| Component | Technology |
|---|---|
| Language | Python 3.11+ |
| MCP SDK | mcp>=1.0.0 (FastMCP) |
| XML Parsing | xml.etree.ElementTree (stdlib) |
| Text Processing | re (stdlib regex) |
| Hashing | hashlib (stdlib SHA-256) |
| Data Models | dataclasses (stdlib) |
| Config Format | JSON |
| Testing | pytest, pytest-asyncio, pytest-cov |
| Formatting | black (line-length 100) |
| Linting | ruff |
| Type Checking | mypy |
| CI | tox (py311 + py312) |
| Package Build | setuptools + wheel |
CLIENT INTEGRATION
Works with any MCP client via stdio transport. One install, any editor.