TIATool

tiax-agent: MCP server, persistent engines, trust controls and licensing (0.8)

 Claude Desktop / Claude Code / Cursor / VS Code / Windsurf / Cline / Codex / Gemini
        │  MCP (stdio, or HTTP 127.0.0.1 + bearer)
        ▼
 tiax-agent.exe  (.NET 10 self-contained, official ModelContextProtocol SDK 2.2)
   • argument rails → policy → licence gate → approval, in that order, on every call
   • cache tools in-process (same catalog as `tiax mcp`), plus agent-only cache_query (Jint)
   • script compiler (Roslyn): scripts become net48 DLLs here; the engine only loads finished DLLs
   • toolsets / find_tool, MCP annotations, structuredContent, elicitation
   • EngineRouter: picks the engine by project extension (.ap19 / .ap21)
        │  named pipe, newline JSON, per-user ACL + shared secret
        ▼
 tiax.exe engine / tiax-v21.exe engine   (net48, Openness)
   • PersistentSession: one STA thread keeps TIA Portal + the project open between calls
   • runs the normal tiax verbs in-process → same backup / guard / exit-code rails
        │ fallback when no engine can start
        ▼
 tiax.exe <verb>   one-shot child (0.3 behaviour)

Model and template tools (0.5, in-process)

template_list, template_show, model_validate, model_generate, model_from_io_list and template_extract run in the agent's cache lane — no TIA, no engine. They read and write only the workspace (model/, build/<batch>/, templates/, assumptions.md, the journal). Getting the generated SCL into a project is a normal build_from_source call (engine, licensed, backup first). The build_from_model prompt chains the whole flow as plan tasks.

Scripts, extensions and cache queries (0.8, Labs)

script_check and script_run compile C# in the agent with Roslyn against the net48 reference assemblies and the project's own Openness assemblies (registry → default install folder; TIATOOL_OPENNESS_REFS / TIATOOL_NET48_REFS override), check the guard rail and cache the DLL by content hash (%LOCALAPPDATA%\TIATool\scripts, TIATOOL_SCRIPT_CACHE). The DLL runs through the tiax script verb on the normal live path (engine or one-shot child): read mode inside a transaction that is never committed or saved; write mode after a person's approval, with backup, commit on success, save, journal and the post-write pipeline. extension_run does the same for a saved extension (mode from its manifest); extension_save promotes a script that ran in this session. cache_query runs JavaScript over the served cache (Jint: 5 s, 64 MB, no .NET access). The legacy net48 tiax mcp answers these tools with "requires tiax-agent". Guide: self-extension.md.

Policy, approvals and the journal (0.8)

Policy. %ProgramData%\TIATool\policy.json (administrators; /etc/tiatool/policy.json on Linux; TIATOOL_POLICY only where none is installed) over <workspace>\policy.json. Fields: allow_tools, deny_tools, read_only, allow_online (default false), allow_scripts (off|read|write), allowed_project_roots, allowed_output_roots, require_confirmation, max_writes_per_hour, confirm_mode (elicitation|out_of_band|in_band). Checked in CallToolCore after the argument rails and before the licence; tools refused whatever their arguments are left out of tools/list; an invalid file refuses every write, online call and script (reads keep working). tia_status and workspace_status show the effective values with their source. On the command line:

tiax-agent policy show [--workspace <dir> | --project <file.apXX>] [--json]   # every value + the file that set it
tiax-agent policy init [--workspace <dir> | --machine] [--allow-online]
                       [--confirm-mode elicitation|out_of_band|in_band] [--force]

init --machine needs an elevated prompt and restricts %ProgramData%\TIATool to Administrators and SYSTEM before writing; an existing file is replaced only with --force (timestamped copy kept). Examples and every field: security/policy.md. The policy governs MCP calls; the command lines are not policed by it.

Approvals. plc_download, restore_backup mode=in_place, script_run / extension_run in write mode, openness_set, openness_invoke and whatever require_confirmation lists need a person's yes:

  • elicitation (client declares it): the plan is shown as a form; the model never sees a token. 2026-07-28 clients get an input-required result whose round-trip state is HMAC-bound to the tool and arguments, expires and is single use; older clients get a server-to-client request. No answer in 10 min = no; a client that cannot ask falls back to out of band;
  • out of band (otherwise, and always for the legacy tiax mcp): the result names APR-XXXXXXXX; the user runs tiax-agent approve <id> (shows the plan, needs a terminal and yes; --deny, --list), then the same call is repeated. Requests bind to the arguments' SHA-256, expire after 30 min, single use (%LOCALAPPDATA%\TIATool\approvals, TIATOOL_APPROVALS);
  • in band (the pre-0.8 confirm_token round trip) only under confirm_mode: in_band.

Journal. Every entry records os_user, host, mcp_client, the approval (channel, approver) and prev_hash (SHA-256 chain). Without a configured workspace the first write to a project creates one under %LOCALAPPDATA%\TIATool\workspaces\<sha1(project path)> (TIATOOL_WORKSPACES), so journal and post-write verify are on by default. tiax-agent journal verify|export [--csv] (same as tiax journal): exit 0 intact, 2 problems, 1 no journal.

Persistent session rules

  • The project stays open between calls; a call for another project closes the previous one first.
  • Before a backup zip is taken the project is released (SessionCoordinator.ReleaseProject) so the zip never contains a half-written file.
  • Any exception resets the session (close project, dispose Portal). A timeout kills the Portal processes the engine owns and restarts the STA thread.
  • Idle timeout (--idle-minutes, default 15) closes Portal to free the Openness licence/RAM.
  • The running-Portal guard ignores Portal processes owned by the engine; a UI instance still blocks.
  • If the engine dies after a verb was sent, the agent reports the failure and does not retry (the write may be half-done; the backup zip is the restore point). If the engine can't be reached before the verb is sent, the call transparently runs as a one-shot child.

Engine protocol

→ {"id":1,"op":"hello","secret":"…"}         ← {"id":1,"exit":0,"result":{"flavour":"V21",…}}
→ {"id":2,"op":"run","argv":["compile","C:\\p\\x.ap21"]}
← {"id":2,"log":"…"} …                       ← {"id":2,"exit":0}
ops: hello | ping | status | run | reset | shutdown

HTTP transport

tiax-agent mcp --http [--port 8765] listens on 127.0.0.1 only. Every request needs Authorization: Bearer <token> (tiax-agent token, stored in %APPDATA%\TIATool\agent.json, --rotate to change). Browser Origins other than localhost are refused (DNS-rebinding guard).

Licensing

  • Token: TT1.<b64url(claims)>.<b64url(Ed25519 sig)>, claims lic sub plan seats mid iat exp pend v plus, since 0.8, optional features[] and kid. Signed by the web backend, verified offline (BouncyCastle) against the embedded key set LicenseKeys.Production (kid missing = k1; an unknown kid says "update TIATool"). Vectors: tests/vectors/license-v1.json, license-v2.json. Every 0.5–0.7 token keeps working.
  • Plans: monthly, yearly, trial, offline, edu, nfr, enterprise. A token without features[] grants every paid feature; with it, writes need write (LicenseStatus.CanWrite). scripts is defined for script_run / extension_save but not yet checked by them.
  • exp is set by the server: never past the paid period end plus the plan's grace (30 days for yearly, enterprise and offline plans, 7 otherwise). A running mcp session refreshes in the background (LicenseRefresher: 15-min timer, due after 24 h, back-off 5 min → 6 h; tool calls never wait for the network). Offline > 24 h = grace (still writes, warns); past exp = every tool refuses. A token is cleared only on an explicit 403/404 from the licence's own server.
  • Unlicensed / expired / invalid = no tools: every MCP tool and every tiax verb except doctor, setup, license, help and version refuses with one "activate or buy" message (LicensePolicy.NoLicenseMessage: tiax-agent license activate <key>, or buy at EUR 99.99/month or EUR 999.99/year). tiax-agent doctor, init, license and setup always work.
  • With an active licence, the read/write split decides which feature a call needs: reads (cache and query tools, honoured previews dry_run / what_if, restore_backup list / new folder, project_archive / project_retrieve, plc_compare_online, scl_lint) need no feature; anything that modifies a project needs write.
  • There is no trial. tiax-agent license trial is refused (403 no_trial). Buy a seat, then license activate. license request-offline --key <K> writes a tiatool.offline-request/1 file for an air-gapped PC; license import <file> installs the tiatool.offline-license/1 answer (verified offline, machine-bound, never refreshed). update --check [--channel stable|beta] reports a newer release; it never installs.
  • Machine id = SHA-256("tiatool-machine-v1:" + Windows MachineGuid)[0..16], hex. One PC per seat (MACHINES_PER_SEAT on the server, default 1).
  • Servers, in order: --server, TIATOOL_LICENSE_SERVER, the server recorded at activation, the website, https://tiatool-web.vercel.app (permanent alias). Failover on network errors, timeouts, 5xx and non-JSON answers; a JSON 4xx is final. The system proxy is used with the user's default credentials (NTLM/Kerberos); Windows credentials are never sent to the licence server.
  • Dev builds only (TIATOOL_DEV_OVERRIDES): TIATOOL_LICENSE_FILE, TIATOOL_LICENSE_PUBKEY, TIATOOL_MACHINE_ID; a release ignores them.
  • Before release: the production public key (npm run keygen in the web app) is LicenseManager.ProductionPublicKey (k1); further keys go into LicenseKeys.Production under a new kid.

Setup

tiax-agent setup --tiax <cache> [--client <list> | --all] [--toolset <set>] [--http] [--remove] [--dry-run] merges a tiatool server into Claude Desktop, Cursor, VS Code, Claude Code (~/.claude.json), Windsurf, Cline, Codex CLI (config.toml) and Gemini CLI — paths and formats in INSTALL.md (McpClientConfig). It keeps other servers and the user's own keys in our entry, drops the obsolete env.MCP_TIMEOUT, takes a new timestamped .bak before each change, replaces the file atomically and is idempotent. Claude Desktop and Codex always get a stdio entry. --all writes only clients installed for the user. doctor reports per client where TIATool is registered and how long calls can finish there. tiax setup / tiax doctor exist on the net48 CLI too.

tiax-agent init --demo builds a workspace from samples\ (ExportsSmall + model_sample) under %LOCALAPPDATA%\TIATool\demo and registers it as tiatool-demo (DemoWorkspace).

Toolsets and find_tool

tiax-agent mcp --toolset explore|generate|commission|admin|extend|all (comma list; also TIATOOL_TOOLSET; default all) filters only what tools/list shows; every catalog tool stays callable. The map lives in one place, src/TiaTool.Agent/Toolsets/ToolsetMap.cs (ToolsetTests fails for an unmapped tool):

Toolset For tools/list size (0.8.0, ≈ chars/4)
explore read-only users: cache queries, export/scan, audit, diff, docs 28 tools, ~9.1k tokens
generate templates, model, generators, build/import, compile 40 tools, ~16.2k tokens
commission compile, download, PLCSIM, online state, verify gate, restore 30 tools, ~10.8k tokens
admin workspace bookkeeping, backups, languages, clean-up 16 tools, ~5.8k tokens
extend self-extension: Openness explorer, scripts, extensions, cache queries, learned knowledge 14 tools, ~7.9k tokens
all everything 75 tools, ~31.4k tokens

(Measured by ToolsetTests.CatalogTokenBudget_PerToolset with online tools allowed; the default policy also hides plc_download, plc_online and plc_compare_online.)

find_tool {query, toolset?, limit?} is always listed (cache lane): it ranks the FULL catalog by name and description and answers one line per tool — name(required, optional?) — first sentence [status] {toolsets} (not listed in this session).

CI front-end: analyze

tiax-agent analyze audit|diff|verify runs ProjectAudit, ProjectDiff and VerifyGate on .tiax caches or SIMATIC ML export folders (scanned into a temp cache) — Linux or Windows, no TIA, no licence. --format json|sarif|junit|md (comma list + --out <folder>), --fail-on error|warning|note|none, --source-root (SARIF URIs relative to it), audit --baseline (only new findings fail). Exit 0 ok, 1 findings at/above --fail-on, 2 usage/input error (never 1 for a crash). SARIF 2.1.0: one rule per audit rule / diff kind / verify check, partialFingerprints, block (and network) logical locations, the exported XML as physical location (project-level results point at the export folder). JUnit: one testcase per rule/check. Verify: a failed or INCONCLUSIVE check is an error. Composite action: .github/actions/tiatool-analyze (needs a Linux tiax-agent via agent-path or agent-url; uploads SARIF with github/codeql-action/upload-sarif); example workflow: docs/ci/github-actions-example.yml.

Logs, support bundle, privacy

Rolling log tiax-agent.log (+ .1….4, 1 MB each) in %LOCALAPPDATA%\TIATool\logs ($XDG_STATE_HOME/~/.local/state/TIATool/logs elsewhere; TIATOOL_LOG_DIR, TIATOOL_NO_LOG=1): start line, everything the agent writes to stderr, and each live call's verb, exit code and duration (no arguments). support-bundle zips doctor, versions, redacted agent/licence config, our entry of each client config (never other servers'), logs, policy and selftest overlay; Redactor masks licence keys, TT1 tokens, bearer/tt_ tokens, secret JSON keys, e-mails, and user/machine names (also inside C:\Users\…, /home/…). privacy prints every endpoint; every ProductEndpoints member must be classified in PrivacyReport.Purposes (SupportTests).

Built from docs/agent.md in the product repository. Links to files that are not published here are shown as plain text.