Tools
Tool reference.
Every tool is a typed, stable interface to PLCs.ai. Names are stable, snake_case, and prefixed plcs_. Reads are safe; writes are consequential and may be billable — see Safety & billing.
Inputs below are the agent-visible parameters. The server manages transport details for you: model tools (plcs_ask_project, plcs_propose_code) run synchronously and return one complete result; writes accept an optional idempotency_key (omit it and the server mints one per call); and file uploads take a file_path/file_inline/file_blob_url instead of raw multipart. For a large or locally-generated file, plcs_stage_upload returns a short-lived upload URL you PUT the bytes to — then pass it back as file_blob_url, so the file never streams through the model.
Connect & discover
plcs_health
Authenticated health check
Authenticated connectivity check for PLCs.ai. Returns the organization id resolved from your credential and the actor type. Use this first to confirm the connection works and which organization you are acting as. Requires only a valid credential — no extra permission.
Inputs
plcs_list_projects
List projects
List the PLC projects this credential can see, most-recently-touched first. Returns identity metadata only (id, name, vendor, version, analysis status) — NOT the project source; read source with plcs_get_project_source. Results are filtered to the credential's project scope and paginated via an opaque cursor (pass cursor from a prior response to page). Requires only a valid credential — no extra permission.
Inputs
| Input | Type | Description |
|---|---|---|
team_id | string | Restrict to a single team. |
limit | integer | Page size (default 50, max 200). |
cursor | string | Opaque cursor from a previous response's next_cursor. |
plcs_get_project
Get a project
Get one project's identity and current-version metadata plus a short analysis_status summary (not the full results blob). An unknown or out-of-scope id returns a not-found error. Requires only a valid credential.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
Read source & live data
plcs_get_project_source
Read a project's source
Get the project itself back, in one of two representations via format: parsed (default — the vendor-neutral parsed model as JSON, the same structure for all three platforms) or raw (the current committed version's L5X XML, Siemens ZIP or CODESYS .export, byte-for-byte the customer's own file). There is nothing between the two: no per-routine, per-tag or aggregated-text views — for a QUESTION about the project, call plcs_ask_project instead of pulling the model and reasoning over it yourself. Both formats reflect the LATEST committed version, including in-app edits saved on the platform, so raw never returns a stale originally-uploaded file. NEITHER IS EVER INLINED: both come back as a file path or a short-lived URL, and you read the payload from there. That is deliberate — a real project's parsed model runs from a few MB into the tens of MB, far past what fits in a reply — so budget for reading it in pieces (parse the JSON from the file, or scan it) rather than expecting it in the response. Requires the code_read permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
format | string enum |
plcs_get_live_values
Latest live tag values
Get the latest live tag values for a project, as streamed by a Desktop Companion App (DCA) session. This is PLCs.ai's most differentiated read: real values off the running plant. IMPORTANT: when no DCA is actively streaming, the response is a normal success with live: false and an empty tags object — this is the expected "no live session" state, NOT an error. Requires the hmi_view permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
plcs_get_tag_history
Recent value history for a tag
Get the recent, most-recent-first value history for a single tag (tag is required) from the current Desktop Companion App session. Returns an empty history when no live session has streamed that tag — a normal state, not an error. Requires the hmi_view permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
tag required | string | The tag name to fetch history for. |
limit | integer | Max entries (default 50, max 600). |
plcs_request_live_tags
Request live values for specific tags on demand
Ask the Desktop Companion App (DCA) serving a project to read a SPECIFIC set of live tags on demand — even if the operator never pressed "Start streaming" (the DCA only needs to be connected + signed in + have the project selected). This is a REQUEST SIGNAL, not a data read: it records the tags (verbatim project tag names, de-duped, up to 1000 per call; unknown names are dropped by the DCA) and returns { project_id, live, requested }. live reports whether a DCA can currently serve the project — when false, the request is still recorded but no values arrive until a DCA connects. To get the VALUES, call plcs_get_live_values a few seconds later (the DCA delivers within a few seconds). Naturally idempotent — re-requesting the same tags just extends the window — so no idempotency key is needed. Requires the hmi_edit permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
tags required | array | Tag names to request live values for (verbatim project tag names). De-duped, and never truncated — ask for every tag in a project and every tag is recorded. maxItems is the declared per-request ceiling (the per-message transport shape), so a whole-project tag set fits in one request; unknown names are dropped by the DCA. |
Ask & propose
plcs_ask_project
Ask a question about a project
Ask a question about a PLC project and get back a grounded answer from PLCs.ai's own PLC-specialized assistant, in one blocking call. THIS IS THE PRIMARY TOOL for any question about what a project does, why it behaves a certain way, or what would break — it reads the project deeply (logic, cross-references, tags, the project graph) and reasons over it for you. Prefer it over assembling your own answer from raw source. Read-only: it creates NO version and changes nothing. Two successful shapes, keyed on status: - { status: "answer", answer, citations, conversation_id, usage } — the answer, plus what it was READ FROM. Each citation names one location the assistant actually looked at: { location_kind, path }, with rung when the read was rung-level and station on a production line. RELAY THEM alongside the answer — for a plant engineer, "which routine did you read?" is the difference between an answer they can act on and one they have to re-verify by hand. A path is the spelling the platform uses, so you can pass it straight back to another tool. Empty means the turn read nothing citable; it never means the answer was made up. - { status: "needs_input", questions, conversation_id, usage } — the assistant needs something clarified before it can answer well, and did NOT guess. Each question has options, a recommended_index hint (may be null), and why_it_matters. ASK THE HUMAN these questions, then call this tool again with the SAME conversation_id and answers: [{ id, selected_index }], or [{ id, free_text }] if none of the options fit. Answer from recommended_index yourself only when the human has already made their intent clear or is unavailable. A third shape, { status: "unresolved", reason }, means you asked it to CHANGE the project rather than asking about it — use plcs_propose_code instead; reason says so. It comes back immediately, before any analysis is billed. Pass conversation_id back on follow-up questions too, so earlier turns stay in context. If the organization is paused at its API spend limit the call fails with a spend-limit error — surface it to the human (they can raise the limit in Settings) and do not retry. Requires the ai_explain permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
prompt | string | The question to ask about the project. |
conversation_id | string | Returned by every response. Pass it back to answer questions or to continue the same line of enquiry with prior turns in context. |
answers | array | Answers to the questions a prior turn returned. |
idempotency_key | string | Optional idempotency key (e.g. a UUID). Normally omit it — the server generates one per call. Pass the SAME key to safely retry this write without double-billing: a replay returns the original result. |
plcs_propose_code
Propose a code change (plan, then author)
Propose a change to PLC logic. CRITICAL SAFETY PROPERTY — it PROPOSES, NEVER DEPLOYS: no call here creates a version or changes anything in the project. Persisting is a separate, explicit step (plcs_save_version, which needs the code_write permission). This is a TWO-STEP FLOW, because a change to running plant logic should be reviewed before it is written: 1. Send prompt (what to change). You get { status: "plan", plan, conversation_id } — the steps it would take, plus assumptions and risks. NO code has been written yet. 2. SHOW THE PLAN TO THE HUMAN, including any risks, and get their approval. Then call again with the SAME conversation_id and approve: true (add amendment for a plain-language tweak). You get { status: "code", explanation, changes } — one entry per changed unit. Both the plan and the code step carry citations — the locations the assistant read while working out the change, in the same shape plcs_ask_project returns. Show them with the plan: they are what the human reviewing the blast radius needs in order to check it. Do NOT approve on the human's behalf. The plan step exists so a person sees the blast radius first; the server then executes the plan it recorded in step 1, so what gets authored is exactly what was reviewed. Two other outcomes: { status: "needs_input", questions } — answer them as with plcs_ask_project (same answers shape, same conversation_id); and { status: "unresolved", reason } — nothing could be proposed, and reason says what to try. All three platforms are supported, and a change's code_type says what it is: for CODESYS that is ST (the unit's complete Structured Text body), Declaration (its complete declaration), LD or Task. An LD content is a rung-edit script against the numbered rungs of the existing body and a Task content is a program name or task fields — NEITHER is source, so do not show either to the human as replacement code. If the organization is paused at its API spend limit the call fails with a spend-limit error — surface it to the human and do not retry. Requires the ai_generate permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
prompt | string | What to generate or change (e.g. "Add a 5-second start-up delay timer"). |
target | object | Optional hint for which program/routine to target. Folded into the prompt as guidance; the assistant decides the final placement. |
target.program | string | — |
target.routine | string | — |
conversation_id | string | Returned by every response. Required to approve or to answer questions. |
approve | boolean | Author the code for the plan pending on conversation_id. The plan executed is the one the server recorded when it was proposed, so what gets authored is exactly what was reviewed. 404 if no plan is pending. |
amendment | string | An optional plain-language tweak applied while executing the approved plan (e.g. "use a latch instead of a seal-in"). |
answers | array | Answers to the questions a prior turn returned. |
idempotency_key | string | Optional idempotency key (e.g. a UUID). Normally omit it — the server generates one per call. Pass the SAME key to safely retry this write without double-billing: a replay returns the original result. |
Analyze
plcs_get_project_analysis
Read the project's analysis
Read the analysis the platform has already run on a project — dead code, missing handshakes, cycle-time and the other checks. You CANNOT start one: analysis is expensive and per-version, so producing it is a deliberate human act — someone analyzes the project in the PLCs.ai app, or a person or script calls the re-run endpoint (POST /projects/{id}/analyses), which is deliberately not a tool. This tool reads a stored result — the project's current version by default — and is a read (no charge). Check status before using results: - complete — results holds the analysis. - running / queued — a run is in flight; wait a few seconds and call again. This is NORMAL progress, not a failure. - not_analyzed — nothing has ever been analyzed for this version, and nothing you can call will start one. Also NOT an error, and it is the EXPECTED state right after plcs_save_version, which commits a version without analyzing it. Do not retry in a loop: say plainly that this version has no analysis and that a person has to run one. - error — the run failed, and failed_phases names which phases did. failed_phases can also appear alongside complete, when only some phases failed — the results are still worth reading, just incomplete. AN OLDER VERSION'S ANALYSIS — READ IT, BUT NEVER PRESENT IT AS CURRENT. On not_analyzed the response may carry last_analyzed_version_id and last_analyzed_at: an analysis exists for an OLDER version of this project. Call again with version_id set to that id to read it. Every response says which version it describes (version_id) and whether that is still the project's current one (is_current_version). When is_current_version is false, those findings describe CODE THE PROJECT NO LONGER CONTAINS. They are specific claims about specific code ("missing handshake between Filler and Capper"), so the change that superseded that version may already have FIXED a finding you would be sending someone to chase — or INTRODUCED one this analysis never saw, so "no findings" does not mean clean. You MUST: - say which version it describes and that the current version has not been analyzed; - never state or imply it is the project's current state, and never answer "is this project safe / clean / correct now?" from it; - say that the only way to get a current analysis is a person running POST /projects/{id}/analyses, which is deliberately not a tool — you cannot start one and neither can retrying this. If you only need to know WHETHER an analysis is ready, plcs_get_project returns an analysis_status summary without the results blob — prefer it for a cheap freshness check. Requires the analysis_tab permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
version_id | string | Read this version's stored analysis instead of the current version's. Must be a version of the project in the path. Omit for the current version. |
Save
plcs_save_version
Commit a new version (Save project)
Persist a NEW version of an existing project from an updated vendor file — the headless equivalent of "Save project". IRREVERSIBLE & CONSEQUENTIAL: a successful commit bumps the project's current version — so a client may ask you to confirm first. It does NOT analyze: the reply carries analysis: "not_analyzed", and plcs_get_project_analysis will report that for the new version until a person runs one. Report that plainly instead of looking for a way to trigger it — there is none on this surface. Provide the file ONE of three ways: file_path (a path on the machine running this server, read in on-prem/local mode), file_inline (base64-encoded bytes), or file_blob_url (a URL the API can fetch). The server picks inline-vs-large-file transport by size automatically — you do NOT base64 large files yourself. originalFilename is optional when using file_path (derived from the path). If you GENERATED/edited the file locally and this server is remote (no shared disk for file_path), do NOT use file_inline — base64 costs a token per byte. Instead call plcs_stage_upload first, PUT the bytes to the upload_url it returns, then pass its file_blob_url + originalFilename here (token-free). The uploaded file MUST be the same vendor as the project (an L5X stays L5X, a Siemens ZIP stays Siemens, a CODESYS .export stays .export) — a mismatch returns a 400. Re-sending the exact current bytes is a safe no-op that returns 200 with resolution: identical_file and NO new version. This only persists what you give it — author a change first with plcs_propose_code, review it, THEN save. Requires the code_write permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
name | string | Optional display name for the project. |
idempotency_key | string | Optional idempotency key (e.g. a UUID). Normally omit it — the server generates one per call. Pass the SAME key to safely retry this write without double-billing: a replay returns the original result. |
originalFilename | string | The vendor file name (e.g. "Injection_Molding.L5X" or "Reference_Coiler.zip"). Optional when using file_path — derived from the path basename. |
file_path | string | Path to the vendor file on the machine running this MCP server (on-prem/local mode). The server reads it and handles upload sizing. |
file_inline | string | Base64-encoded file bytes — an alternative to file_path when the file is not on the server's filesystem. Large files are routed to the large-file flow automatically. |
file_blob_url | string | A URL the PLCs.ai API can fetch the file bytes from (large-file escape hatch). Provide originalFilename alongside it. |
plcs_stage_upload
Stage a file upload (token-free)
Stage a TOKEN-FREE file upload for a subsequent plcs_save_version. Use this whenever the file you want to save was generated or edited LOCALLY and this MCP server is remote — so file_path (which needs a shared disk) is unavailable and file_inline would cost a token for every byte of the file. It returns an upload_url: PUT the raw file bytes there over plain HTTP (e.g. curl -T <file> "<upload_url>") so the bytes never pass through the model. It also returns a file_blob_url that the PLCs.ai API fetches the bytes from. After uploading, call plcs_save_version with that file_blob_url and originalFilename. The links are short-lived. This creates NO project version on its own, is NOT billable, and makes no change to any project.
Inputs
| Input | Type | Description |
|---|---|---|
filename required | string | The vendor file name to stage, e.g. "PG_CM13.L5X" or "Reference_Coiler.zip". |
Export
plcs_export_plc
Export the project to its vendor PLC file
Start an asynchronous export of a project's current committed version to its native vendor format (a Rockwell .L5X, a Siemens TIA .zip or a CODESYS .export) and get back a job id immediately — this does NOT block. Returns { exportId, status }; then poll plcs_get_export with that exportId until status is complete, and fetch the bytes with plcs_download_export. Exports the current version as-is (no edits). A CODESYS export is written by patching the .export the version was uploaded from, so if that file is no longer in storage the JOB finishes with status: "error" — the immediate reply is still a success, so always poll for the terminal state rather than assuming the artifact exists. Requires the export_plc permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
idempotency_key | string | Optional idempotency key (e.g. a UUID). Normally omit it — the server generates one per call. Pass the SAME key to safely retry this write without double-billing: a replay returns the original result. |
plcs_export_pdf
Render a PDF report for the project
Start an asynchronous render of a PDF documentation report for a project's current committed version (it does NOT include in-app chat history). Returns { exportId, status } immediately; poll plcs_get_export until complete, then fetch the bytes with plcs_download_export. Requires the export_pdf permission.
Inputs
| Input | Type | Description |
|---|---|---|
id required | string | The project id. |
idempotency_key | string | Optional idempotency key (e.g. a UUID). Normally omit it — the server generates one per call. Pass the SAME key to safely retry this write without double-billing: a replay returns the original result. |
plcs_get_export
Poll an export job
Poll the status of an export job started by plcs_export_plc / plcs_export_pdf, by its exportId. While pending/running the response carries a message with poll guidance and no artifact yet — keep polling, this is normal, not an error. When status is complete, download_url (a relative path) and filename are populated — but prefer plcs_download_export to actually retrieve the bytes. When error, error carries a short reason. This is a read (no charge). Gated on the export's matching permission (export_plc / export_pdf).
Inputs
| Input | Type | Description |
|---|---|---|
exportId required | string | The export job id returned by startPlcExport / startPdfExport. |
plcs_download_export
Download a completed export artifact
Retrieve the finished artifact (L5X / Siemens ZIP / CODESYS .export / PDF) of a complete export job by its exportId. The bytes are handed back as a local file path (on-prem) or a short-lived signed URL (cloud) — NEVER inlined into the response, so a large ZIP/PDF never floods the conversation. Read the artifact from the returned path/URL. Call plcs_get_export first and only download once status is complete. Gated on the export's matching permission.
Inputs
| Input | Type | Description |
|---|---|---|
exportId required | string | The export job id returned by startPlcExport / startPdfExport. |
Embed
plcs_mint_embed_token
Mint a read-only embed token
Mint a short-lived, project-scoped, read-only token for the embeddable assistant iframe. Regardless of your key's scope the token is intersected down to read-only (ai_explain + hmi_view) — a browser-delivered token can never carry a write scope. Returns { token, expires_at, permissions, project_id }. The minting key must itself have at least ai_explain.
Inputs
| Input | Type | Description |
|---|---|---|
project_id required | string | The project the embed token may access. |
idempotency_key | string | Optional idempotency key (e.g. a UUID). Normally omit it — the server generates one per call. Pass the SAME key to safely retry this write without double-billing: a replay returns the original result. |