🟑 Developer β€’ Published β€’ v1.0.0

twincat-validator-mcp

Deterministic validation and auto-fix for LLM-generated TwinCAT 3 PLC code

terminal
$ pip install twincat-validator-mcp
βœ“ Zero external dependencies  Β·  βœ“ Python 3.11+  Β·  βœ“ MIT License  Β·  βœ“ stdio MCP transport
1.0.0
Version
Python 3.11+
Language
stdio
Transport
16
MCP Tools
34
Val. Checks
MIT
License

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.

34 check definitions
πŸ”§

Auto-Fixes

9 categories of common issues in a dependency-aware, idempotent pipeline. Run twice β€” byte-identical output.

9 fix operations
πŸ—οΈ

Scaffolds

Canonical XML skeletons for new file types from generation contracts. No hand-written boilerplate.

4 file types supported
πŸ”„

Orchestrates

Full validate β†’ fix β†’ re-validate β†’ determinism-verify pipelines with loop prevention built in.

Max 3 correction iterations
πŸ“‹

Enforces OOP

IEC 61131-3 OOP contracts β€” 21 checks β€” with configurable per-project policy via .twincat-validator.json.

16 configurable policy knobs
πŸ“š

Provides Knowledge

LLM-friendly knowledge base, generation contracts, and 8 reusable prompt templates baked into the server.

11 resource endpoints

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.

.TcPOU
Program Organization Units
Function Blocks, Programs, Functions
.TcIO
I/O Configurations
Interfaces
.TcDUT
Data Unit Types
Structures, Enums, Type Aliases
.TcGVL
Global Variable Lists
Shared global variables

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.

LLM Client Layer
Claude Desktop, Cursor, VS Code Copilot, Windsurf, Cline
Any MCP-compatible client connects via stdio. Sends tool calls, reads resource endpoints, uses prompt templates.
MCP Interface Layer
server.py, mcp_app.py, mcp_tools_validation.py, mcp_tools_fix.py, mcp_tools_batch.py, mcp_tools_orchestration.py, mcp_resources.py, mcp_responses.py, prompts.py
Thin facade. Registers all tools, resources and prompts. Handles input validation and parameter normalization. Formats response envelopes with policy proofs.
Engine Layer
engines.py (ValidationEngine, FixEngine), result_contract.py (ContractState), policy_context.py (ExecutionContext)
Orchestrates check/fix execution. ContractState is the single source of truth for safe_to_import / safe_to_compile flags. ExecutionContext carries the policy proof envelope.
Check / Fix Layer
validators/ (base, xml, guid, structure, style, naming, oop), fixers/ (base, simple, structural, complex, oop)
Auto-discovered via decorator registry. Each check/fix is isolated in its own try/except β€” one broken check never aborts the pipeline.
Domain Layer
file_handler.py, models.py, oop_index.py, snippet_extractor.py, utils.py, exceptions.py
TwinCATFile is the core value object β€” lazy loading, cache invalidation on mutation, always parses from in-memory content (never stale disk).
Configuration Layer
validation_rules.json, fix_capabilities.json, naming_conventions.json, knowledge_base.json, generation_contract.json
All rules, fixes, naming conventions and knowledge base entries live in JSON β€” non-developers can review and modify without touching Python code.

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.

PHASE 1 β€” REQUEST Prompt β†’ Tool call β†’ File load β†’ Engine dispatch
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.

PHASE 2 β€” RESPONSE Engine results β†’ Contract state β†’ Envelope β†’ Client
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.

5
Validation Tools
3
Fix & Generate Tools
2
Batch Tools
6
Orchestration Tools

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

validation-rules:// All 34 check definitions
fix-capabilities:// All 9 fix definitions with complexity and risk
naming-conventions:// TwinCAT naming patterns by file type
config://server-info Server metadata and capability summary
knowledge-base:// Full LLM-friendly knowledge base
knowledge-base://checks/:check_id Per-check explanation, examples, mistakes
knowledge-base://fixes/:fix_id Per-fix algorithm and before/after examples
generation-contract:// Deterministic generation contracts
generation-contract://types/:file_type Contract for a specific file type
oop-policy://defaults Default OOP policy values
oop-policy://effective/:target_path Resolved policy for a path

prompts.py 8 templates

validate_and_fix
One-shot validate β†’ autofix β†’ report
prepare_for_import
Import safety check with blocker explanation
check_oop_compliance
Full OOP review with knowledge-base excerpts
batch_normalize
Pre-commit normalization of all TwinCAT files
check_naming_only
Naming convention compliance review
fix_then_verify
Final step before commit: fix + validate
generate_and_validate
Create skeleton + validate
explain_check
Developer learning: explain a check from the knowledge base

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.

Python
@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.

Python
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.

Critical β€” Blocks import
Warning β€” Advisory
OOP β€” Runs when EXTENDS/IMPLEMENTS detected

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)

Inheritance Safety
β€Ί extends_visibility
β€Ί extends_cycle
β€Ί diamond_inheritance_warning
Override Correctness
β€Ί override_marker
β€Ί override_signature
β€Ί override_super_call
Interface Compliance
β€Ί interface_contract
β€Ί inheritance_property_contract
β€Ί interface_segregation
FB Lifecycle
β€Ί fb_init_signature
β€Ί fb_init_super_call
β€Ί fb_exit_contract
Memory Safety
β€Ί dynamic_creation_attribute
β€Ί pointer_delete_pairing
Design Quality
β€Ί this_pointer_consistency
β€Ί abstract_contract
β€Ί abstract_instantiation
β€Ί composition_depth
Property / Method
β€Ί property_accessor_pairing
β€Ί method_visibility_consistency
β€Ί method_count

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.

1
Tabs to Spaces
Must run first β€” indentation depends on this
2
File Ending
Ensures closing TcPlcObject tag is present
3
Property Newlines
Normalizes newlines around property elements
4
CDATA Formatting
Standardizes CDATA section whitespace
5
Property VAR Blocks
Inserts missing VAR/END_VAR in getters
6
Excessive Blank Lines
Caps consecutive blank lines at 2
7
Indentation
Enforces 2-space indent β€” runs after tabs are fixed
8
GUID Case
Normalizes GUIDs to lowercase hex
9
LineIds
Experimental β€” rebuilds LineIds metadata
Idempotency Guarantee

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.

PHASE 1 β€” SETUP Load context β†’ Scaffold β†’ Write β†’ Submit
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.

PHASE 2 β€” CORRECTION LOOP Process β†’ Check safety β†’ Fix or stop (max 3 iterations)
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).

Max iterations
3 correction passes per file
No change
no_change_detected == true and file still unsafe
No progress
issue_fingerprint repeats with no_progress_count >= 2
PHASE 3 β€” VERIFICATION Second pass confirms byte-identical output (determinism check)
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

Critical / Error βˆ’25 pts
Warning βˆ’5 pts
Info βˆ’1 pt

Score Ratings

90–100 Excellent β€” production ready
70–89 Good β€” minor issues
50–69 Needs work
0–49 Critical issues present

TECH STACK & METRICS

Zero external dependencies beyond the MCP SDK. The entire validator uses only Python standard library.

Zero External Dependencies

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.

21
Source Modules
Python files
5
Config Files
JSON
40+
Test Modules
pytest
34
Val. Checks
21 OOP checks
9
Auto-Fix Ops
deterministic
16
MCP Tools
tool handlers
11
MCP Resources
read-only
8
MCP Prompts
templates
16
OOP Policy Knobs
configurable
10+
TwinCAT Files
test fixtures
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.

πŸ€–
Claude Desktop
One-click .dxt extension
⚑
Cursor
.cursor/mcp.json config
πŸ’™
VS Code
.vscode/mcp.json config
🌊
Windsurf
~/.codeium/windsurf/mcp_config.json
πŸ”§
Cline
VS Code extension MCP config
πŸ”Œ
Any MCP Client
stdio transport β€” JSON-RPC over stdin/stdout