Licence keys going out. Counting down to launch.

Stop your agent guessing
at TwinCAT code.

plcsense gives AI coding agents a correct, read-only view of your TwinCAT solution and every library it references: resolved symbols, real library APIs, version-aligned documentation and Structured Text diagnostics.

It runs as a process on your machine: your code is never uploaded for analysis. No TwinCAT XAE on the read path, and nothing written back to your project.

  • TwinCAT 3
  • Structured Text
  • IEC 61131-3
  • MCP
  • read-only
  • runs locally
plcsense
$ plcsense tool list_plc_libraries
Solution SealingCell.sln · 1 PLC project

SealingCell (1 library)
  AcmeSealingLib 1.0.0.0 · resolved (reference) · 4 types

$ plcsense tool read_plc_source --symbol_name FB_SealHead
FunctionBlock FB_SealHead · __library__/FB_SealHead.st
[source=vendor_library, confidence=source_provided,
 library=AcmeSealingLib]

Declaration · L4-15
FUNCTION_BLOCK FB_SealHead
VAR
    _phase    : E_SealPhase;
    _dose     : ST_SealDose;
    _tripCode : UINT;

END_VAR

$ plcsense tool validate_plc_project
Ok · 1 project · 2 types · 0 errors

Real CLI output against the demo solution. Agents call the same tools over MCP.

Launch countdown

This round's licence keys are going out

We are heads-down getting plcsense ready for a public launch, targeting 29 October 2026. Leave your email below and we will write once, the day it goes live.

Get notified at launch

plcsense is targeting a 29 October 2026 launch.

The problem

Agents fail at PLC code because they guess

Ask an agent to touch Structured Text and it will call methods that do not exist, hang a real method on the wrong type, invent enum values and mix up library versions. That is not a typing problem. What it needs is scattered across compiled archives and whatever a build happens to leave behind, never the whole library, and nothing tells it which is which.

  1. Libraries are archives, not source

    The types your code depends on live in referenced .library files, not as source in the workspace. There is nothing there for a language model to read directly, so it fills the gap with plausible member names.

  2. Structured Text is thin in training data

    Models have read orders of magnitude more C# than ST. What comes back looks like the language and does not compile, and the examples they learned from are years behind the libraries you use.

  3. Version pins are invisible

    Your solution references one specific version of each library. Nothing in the prompt says which, so an agent happily mixes in an API from a release you never installed.

  4. The method is real. The type is wrong.

    In a large solution the same helper exists on a sibling type. Grep finds it. The agent hangs the call on the type you asked about. The line looks like Structured Text. It will not compile.

Actual examples

Real prompts, real answers

Each slide starts with a prompt you would type to an agent. Both sides show the answer it gives. Left is without plcsense, from the project files and the build dump alone. Right is with it. When the prompt asks for code, that answer is Structured Text, and the left side would not compile.

Example

Prompt

Write ST that starts Service_11 automatically from GVL_MTP.

Without plcsense

GVL_MTP.Service_11.StartAutomaticAsync(0);

In a large MTP project other services declare StartAutomaticAsync. Grep finds those helpers, so the agent hangs the call on Service_11.

With plcsense

IF GVL_MTP.Service_11.SetAutomaticInternal() THEN
  GVL_MTP.Service_11.ProcedureInt := E_MTP_P_Service_11.Proc_11_Automatic;
  GVL_MTP.Service_11.CommandInt := E_MTP_Command.Start;
END_IF

FB_MTP_Service_11 does not declare that method. Switch with SetAutomaticInternal, then set ProcedureInt and CommandInt.

How the right side knew read_plc_source · FB_MTP_Service_11
FUNCTION_BLOCK FB_MTP_Service_11
METHOD PUBLIC Idle : BOOL
METHOD PUBLIC ToOffline : BOOL
METHOD PUBLIC ToOnline : BOOL

project-local FB_MTP_Service_11 · ZComponents

Prompt

Reset the MTP PID pid. pid is a Tc3_MTP.FB_MTP_PIDCtrl.

Without plcsense

pid.Reset();

This solution already has a ZCore PID with Reset(). The agent hangs that call on the MTP controller.

With plcsense

pid.ResetPID();

The method is ResetPID. There is no Reset on this function block.

How the right side knew read_plc_source · FB_MTP_PIDCtrl
FUNCTION_BLOCK FB_MTP_PIDCtrl
METHOD ResetPID : BOOL

Tc3_MTP

Prompt

Convert unixTs to a string. This project has ZAux and ZPlatform.

Without plcsense

s := builder.AppendUlint(unixTs).ToString();
s := ZPlatform.UlintToStr(unixTs);

The helper is unused, so it is missing from the build dump. The agent invents UlintToStr and stores the result in s. S is a TwinCAT keyword (IL Set), so that name does not compile. validate_plc_project flags it.

With plcsense

str := ZAux.TimestampToString(unixTs);
ZPlatform.TimestampToString(unixTs, str);

Both libraries name it TimestampToString. ZAux returns the string. ZPlatform writes into str. Do not call that variable s.

How the right side knew read_plc_source · TimestampToString
FUNCTION TimestampToString : ZCore.ZString  (* ZAux *)
VAR_INPUT timestamp : ULINT; END_VAR

FUNCTION TimestampToString  (* ZPlatform *)
VAR_INPUT timestamp : ULINT; END_VAR
VAR_IN_OUT str : ZCore.ZString; END_VAR

ZAux 1.6.0.26 · ZPlatform 1.6.0.26

Prompt

Read input word 3 from the cleaning robot into nIn.

Without plcsense

nIn := _robot.Cleaning.GetInData(3);

The method name is on the drive interface. The agent hangs it on Cleaning.

With plcsense

nIn := _drive.GetInData(3);

Cleaning is an IRobot. Call GetInData on the IRobotDrive instance, not on the robot. The robot does not expose that drive.

How the right side knew read_plc_source · IRobotDrive#GetInData
METHOD GetInData : DINT
VAR_INPUT
  inData : UINT;
END_VAR

project-local IRobot · IRobotDrive

Prompt

Switch the ZCore PID _pid to back-calculation anti-windup.

Without plcsense

_pid.SetAntiWindup(ZCore.PidAntiWindup.BackCalculation);

The type is unused, so it is missing from the build dump. The agent invents PidAntiWindup.BackCalculation and a setter that does not exist.

With plcsense

_pid.SetAntiWindupType(ZCore.PidAntiWindupType.ConditionalClamping);

The type is PidAntiWindupType. There is no BackCalculation. The members are IntegralLimitation and ConditionalClamping. The setter is SetAntiWindupType. ConditionalClamping is the one that stops integrating when the output is saturated.

How the right side knew read_plc_source · PidAntiWindupType
TYPE PidAntiWindupType :
(
  IntegralLimitation := 0,
  ConditionalClamping
);

ZCore 1.9.0.1 · Tutorial_Quickstart

Prompt

Which types in this Quickstart implement IHaltable?

Without plcsense

Only StateMachineImpl.
Quickstart sequences do not.

A grep of the build dump finds Implements IHaltable on StateMachineImpl. The agent stops there.

With plcsense

Every ISequence, including
QuickstartSequenceAutomatic.

ISequence extends IHaltable. Every Quickstart sequence is an implementer, even though none of them write the name.

How the right side knew find_plc_implementations · IHaltable
INTERFACE ISequence EXTENDS IObject, IError,
  ICancellationToken, IHaltable

ZCore · Tutorial_Quickstart

Prompt

Copy the name of test into name. test is an FB_Test.

Without plcsense

name := test.GetName();

The build dump lists GetName, so the agent calls it.

With plcsense

(* GetName is INTERNAL; there is no call you can write *)

INTERNAL means only TcUnit itself may call it. Your project will not compile that line.

How the right side knew read_plc_source · FB_Test#GetName
METHOD INTERNAL GetName : T_MaxString

TcUnit_Zeugwerk 1.3.2.0

Prompt

Force bFlag to TRUE in this TcUnit test. The variable is write-protected.

Without plcsense

WRITE_PROTECTED_BOOL(Ptr := bFlag, Value := TRUE);

The helper is unused, so it is missing from the build dump. The agent passes bFlag directly.

With plcsense

WRITE_PROTECTED_BOOL(Ptr := ADR(bFlag), Value := TRUE);

The library wants a pointer. The call is ADR(bFlag), not bFlag.

How the right side knew read_plc_source · WRITE_PROTECTED_BOOL
FUNCTION WRITE_PROTECTED_BOOL
VAR_INPUT
  Ptr : POINTER TO BOOL;
  Value : BOOL;
END_VAR

TcUnit_Zeugwerk 1.3.2.0

A coding agent is not deterministic. Without plcsense the same prompt can produce a different guess, depending on what it happens to find on disk or on the web. With plcsense the type is in the context, so the same prompt is much more likely to produce the same correct answer. The declaration under each slide is that source.

What it does

Four things agents get wrong, answered from your project

Every answer comes from the solution on disk and the libraries it references, with provenance attached, so you can see where it came from.

Symbol resolution, not grep

An agent with grep usually finds an obvious declaration in your own source just fine. It breaks down when a helper exists on a sibling type, so the search hangs that call on the type you asked about, or when Reset is a real method on a dozen unrelated types and a search for callers returns most of the solution. Which override runs at a SUPER^ call depends on the instance's real type, not a string in the file. plcsense resolves by type, not by text.

Library APIs without XAE

Even your own .library is a compiled archive, not a text file an agent can open. plcsense reads it straight out of the solution, so an agent sees the members and signatures of the exact version your project pins - not whichever commit happens to be checked out in a library repo somewhere else.

Documentation that matches the version

Standard Beckhoff libraries get official Infosys prose and hardware specs automatically, no documentation setup required. Publish your own libraries with zkdoc and plcsense adds those too, matched to the version you reference, not last year's export - or point it at Atlassian Confluence Cloud spaces you already have for internal and OEM docs. Everything beyond Beckhoff is opt-in, never mixed into search by default.

Diagnostics before you commit

Validate Structured Text and get syntax errors back in original file coordinates, so an agent can check the edit it just made instead of hoping.

Local by default

Runs on your machine

plcsense is a local process that reads local files. Your source, your proprietary libraries and your customers' projects are never uploaded for analysis, and there is no cloud service in the middle to trust. What does leave the machine is short enough to list in full.

Stays on your machine

  • Your solution and every POU, DUT and GVL in it
  • The .library files it references
  • Documentation you map from disk or your own server, plus optional Beckhoff Infosys and Confluence Cloud
  • Every query your agent asks and every answer it gets

What does leave it

  • Licence check. A periodic call to api.zeugwerk.dev carrying your licence key and machine identity, nothing from your project.
  • Opt-out telemetry. Counters for which tool was called and how often, so we can see what to improve. No code, no symbol or file names. On by default, off with one variable.
  • Opt-in feedback. Only when you start plcsense with --enable-feedback, so an agent can report a result that was wrong. Nothing is sent otherwise.

That's plcsense's side of it. Your agent still hands the same query and answer to its own model to act on, and if you run a cloud-hosted model, your library and source content goes to that provider too. That's your agent's model choice, not plcsense's, and it happens the same way whether or not plcsense is in the loop.

PLCSENSE_TELEMETRY=0

Set that variable and the counters stop, while everything else keeps working. What we store and for how long is in the privacy policy.

Setup

Works where your agent already works

plcsense is an MCP server with a CLI in front of it. One command wires it into a project for the client you use.

  • Cursor
  • Claude Code
  • VS Code
  • Codex
  • Gemini CLI
  • Zed
one-time setup
$ scoop install zeugwerk/plcsense
$ plcsense license activate <key>
$ plcsense setup cursor
.cursor/mcp.json
.Zeugwerk/plcsense.json (created with defaults)

Main path is Scoop plus a licence key. For MCP configs that prefer npm, npx -y @zeugwerk/plcsense mcp works too (same token and licence). Commands may still change before launch.

Boundaries

Read-only by design

plcsense reads. Your editor writes, TwinCAT builds and deploys. That split is deliberate, not a gap we intend to close.

It never writes your project

Agents edit files through the editor. There is no write path into your solution, no code generation and no scaffolding.

No runtime, ADS or deploy

No live PLC access, no symbol read or write over ADS, no download to a target.

Not an IDE replacement

No I/O tree authoring, no EtherCAT scan, no axis configuration. That stays in XAE.

It works on your solution as it is

plcsense is its own product, sold and supported on its own: it reads plain TwinCAT projects and plain library files, so there is no framework to adopt, no project template to follow and nothing else you have to buy. Where you already publish with zkdoc it fits in and reads what your pipeline puts out; where you don't, the library files in the solution are enough.

Practical questions

Getting it running

Install, licence, and how to keep it up to date.

How do I install it?

On Windows, set your download token and run one PowerShell command. That installs plcsense with Scoop and activates your licence when needed.

$env:PLCSENSE_TOKEN = '<your token>'
irm https://plcsense.com/install.ps1 | iex

If you would rather wire an MCP client through npm: same PLCSENSE_TOKEN, Node.js 18+, Windows x64 today, and still a commercial licence.

$env:PLCSENSE_TOKEN = '<your token>'
npx -y @zeugwerk/plcsense mcp

The download token and licence key come with this round's early access, which is now closed to new signups; if you already signed up, keys are going out on a rolling basis, so yours may still be on the way. Join the launch list for the next round. The installer is plain text, so you can read it before you run it.

What do I need on the machine?

Windows, PowerShell and the TwinCAT solution you want to work on. Neither TwinCAT nor XAE has to be installed for plcsense to read your project and its libraries, though it will use your local Managed Libraries folder when there is one, and you can point it somewhere else if your libraries live elsewhere. The npm launcher also needs Node.js 18+.

Which agents does it work with?

Cursor, Claude Code, VS Code, Codex, Gemini CLI and Zed. Run the matching setup command in the folder you open in your agent, with your TwinCAT solution in it or below it, and it writes the MCP entry for that client plus a .Zeugwerk/plcsense.json you can point at your documentation. Project-local only: nothing is written to your global agent config.

plcsense setup cursor
What goes into .Zeugwerk/plcsense.json?

plcsense setup <client> writes .Zeugwerk/plcsense.json once, with every knob present and set to its default, so the file itself documents what you can change instead of hiding it in code. Nothing in it is secret, so it is safe to commit alongside the solution.

{
  "config_version": 3,
  "managed_libraries": {
    "root": "C:/TwinCAT/3.1/Components/Plc/Managed Libraries/"
  },
  "doc_sites": {
    "ZCore": { "root_template": "https://doc.zeugwerk.dev/{channel}/" },
    "AcmeSealingLib": { "root": "https://docs.acme.example/sealing/1.4/" }
  },
  "beckhoff_infosys": {
    "enabled": true,
    "cache_ttl_hours": 168,
    "crawl_delay_ms": 10,
    "extra_eligible_namespaces": []
  },
  "confluence": {
    "enabled": false,
    "base_url": "https://your-site.atlassian.net/wiki",
    "spaces": [],
    "library_namespaces": {}
  },
  "parser": { "backend": "TreeSitter" },
  "libraries": {
    "transitive_references": true,
    "max_depth": 8,
    "max_libraries": 250,
    "wrapper_hint_names": ["ZPlatform", "ZAux"],
    "hydration_profile": "signatures"
  },
  "validation": {
    "semantic_checks": true,
    "disabled_checks": []
  }
}
config_version
Stamp on the shipped preset, not a product version number. setup only rewrites a file whose stamp is behind the current one, and asks first unless told otherwise.
managed_libraries.root
TwinCAT's own Managed Libraries folder. Already resolves %TWINCAT3DIR% by default; set it only if your libraries live somewhere else.
doc_sites.<Namespace>
Where a library's zkdoc output lives, per library namespace, so get_plc_type_documentation reads the version your project actually references. root_template resolves a {channel}/{version} placeholder from that version; root is a fixed URL for a site that does not version its docs.
beckhoff_infosys.*
On by default, pulling official Beckhoff pages for Beckhoff libraries. cache_ttl_hours and crawl_delay_ms are cache and politeness knobs; extra_eligible_namespaces is an escape hatch for a library that carries no Tc2_/Tc3_ name and no distributor tag.
confluence.*
Off until you set enabled. base_url and spaces scope the opt-in Confluence Cloud lane; the token itself is an environment variable, never a line in this file.
parser.backend
TreeSitter by default; Antlr is the fallback grammar engine.
libraries.*
How far library hydration follows references: transitive_references, max_depth and max_libraries bound the walk; wrapper_hint_names flags your own wrapper convention to a validation check; hydration_profile is "signatures" (default, no method bodies) or "full" (source libraries keep their bodies too).
validation.*
semantic_checks turns the extra checks off entirely, for when one misfires more than it helps; disabled_checks silences a single rule code instead.

Everything here already works with no edits. doc_sites is the one most people touch by hand, to point plcsense at their own libraries' documentation.

Is this a language server?

No. There is no LSP, no inline completions, no diagnostics in your editor: plcsense only answers when an agent calls one of its MCP tools. But if you are asking, you have already found the real problem: agents guess on TwinCAT code because nothing gives them the grounded, semantic view of a solution and its libraries that a language server gives for mainstream languages. That is the gap plcsense closes, as an MCP tool rather than an LSP. Tell us what you are working on in the launch list form: understanding that distinction already puts you ahead of most people asking for a demo.

When will I actually notice the difference?

Not on a one-line change to a function block you already know, sitting in the open file. Reading that file is enough there, and an agent guessing from the screen looks just as good.

You notice it the moment the work leaves that file. That includes a few lines against a sibling type, a library you have not called yet, an interface with more than one implementer, a rename across many POUs, or a project someone else wrote.

A text search or the .tmc from your last TwinCAT build can still look correct for names already on that project's symbol tree. Neither covers a helper that exists only on a sibling, the rest of a library you have not called, which override actually runs, or a single view across several PLC projects. find_plc_implementations and find_plc_references answer by type, not by text.

The failure this catches is often a line that looks like Structured Text and does not compile: the method exists, just not on that type. It is also a rename that still compiles with one call site quietly left untouched.

Does it work with CODESYS or SIMATIC AX?

Not today: TwinCAT 3 is what we test on and what we support. Very little of the engine is Beckhoff-specific though. It parses IEC 61131-3 Structured Text, and what knows about a vendor is the layer that finds projects and resolves libraries. For a toolchain that keeps its sources as plain ST files that layer is the whole job, so SIMATIC AX code itself would largely be reachable, while what its packaged libraries contain would not. CODESYS needs its sources readable on disk, so an export rather than the binary project file. Tell us in the form which one you are on: what we do after TwinCAT is exactly the sort of thing the pilot decides.

Is there a Linux or macOS build?

Not yet. Nothing in the analysis needs Windows, but Windows is the only build we package today, so that is the honest answer for early access. Tell us in the form if another platform is what stands between you and using it, because that is the kind of thing the pilot is meant to decide.

How do I update?

Scoop: scoop update plcsense, or re-run the installer. npm: the next npx -y @zeugwerk/plcsense picks up the published version. Expect to update often during early access.

scoop update
scoop update plcsense
What should I ask it, the first time?

Open your own solution and ask about a library you depend on but have not called from yet. That is the case a text search or the .tmc from your last build cannot cover, because the rest of that library never made it onto this project's own symbol tree.

"How do I call FB_SealHead from AcmeSealingLib? What are its methods and parameters?"
"Which types in this project implement [an interface from a library we use]?"
"What are the valid values of E_SealPhase in the AcmeSealingLib version we reference?"
"Where is ST_SealDose used across this solution?"
"Validate this project and fix whatever it finds."

Those are close to the demo output at the top of this page, on purpose: the same tools answer them on your own solution.

How do I get a licence key?

This round of early access is closed to new signups, and we are sending out its licence keys on a rolling basis - everyone in it should have theirs soon. Leave your details in the launch list and we will write when plcsense is publicly available, or sooner if a spot opens before then.