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.

18 toolsStreamable HTTP

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_upload_id instead of raw multipart. For a large or locally-generated file, plcs_stage_upload returns a short-lived upload target you PUT the bytes to with the headers it lists — then pass its file_upload_id to plcs_save_version, so the file never streams through the model.

Connect & discover

plcs_health

Authenticated health check

GET/health
Read-onlyAny valid credential

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

No inputs.

plcs_list_projects

List projects

GET/projects
Read-onlyAny valid credential

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

InputTypeDescription
team_idstringRestrict to a single team.
limitintegerPage size (default 50, max 200).
Default 50 · min 1, max 200
cursorstringOpaque cursor from a previous response's next_cursor.

plcs_get_project

Get a project

GET/projects/{id}
Read-onlyAny valid credential

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

InputTypeDescription
id requiredstringThe project id.

Read source & live data

plcs_get_project_source

Read a project's source

GET/projects/{id}/source
Read-onlyRequires code_read

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

InputTypeDescription
id requiredstringThe project id.
formatstring enum
Values: parsed raw · Default parsed

plcs_get_live_values

Latest live tag values

GET/projects/{id}/hmi/values
Read-onlyRequires hmi_view

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

InputTypeDescription
id requiredstringThe project id.

plcs_get_tag_history

Recent value history for a tag

GET/projects/{id}/hmi/history
Read-onlyRequires hmi_view

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

InputTypeDescription
id requiredstringThe project id.
tag requiredstringThe tag name to fetch history for.
limitintegerMax entries (default 50, max 600).
Default 50 · min 1, max 600

plcs_request_live_tags

Request live values for specific tags on demand

POST/projects/{id}/hmi/request-tags
WriteRequires hmi_edit

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. Pass interval_ms (100–2000) to open an observation window that samples the tags at that rate for duration_sec (60–900, default 180) — the response then adds window_id and sample_interval_ms; one window per project, and a standing one is reported rather than replaced. Requires the hmi_edit permission.

Inputs

InputTypeDescription
id requiredstringThe project id.
tags requiredarrayTag 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.
interval_msintegerSample interval in milliseconds. Present, the call opens an observation window at this rate instead of a bare request, and the response carries window_id and sample_interval_ms.
min 100, max 2000
duration_secintegerHow long the observation window runs, in seconds (1–15 minutes). Only meaningful with interval_ms.
Default 180 · min 60, max 900

Ask & propose

plcs_ask_project

Ask a question about a project

POST/projects/{id}/interpret
WriteIdempotent retryRequires ai_explain

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

InputTypeDescription
id requiredstringThe project id.
promptstringThe question to ask about the project.
conversation_idstringReturned by every response. Pass it back to answer questions or to continue the same line of enquiry with prior turns in context.
answersarrayAnswers to the questions a prior turn returned.
idempotency_keystringOptional 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)

POST/projects/{id}/generate
WriteIdempotent retryRequires ai_generate

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

InputTypeDescription
id requiredstringThe project id.
promptstringWhat to generate or change (e.g. "Add a 5-second start-up delay timer").
targetobjectOptional hint for which program/routine to target. Folded into the prompt as guidance; the assistant decides the final placement.
target.programstring—
target.routinestring—
conversation_idstringReturned by every response. Required to approve or to answer questions.
approvebooleanAuthor 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.
amendmentstringAn optional plain-language tweak applied while executing the approved plan (e.g. "use a latch instead of a seal-in").
answersarrayAnswers to the questions a prior turn returned.
idempotency_keystringOptional 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

GET/projects/{id}/analysis
Read-onlyRequires analysis_tab

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. skipped_phases names phases the run ended WITHOUT — they did not fail, they never ran, so results has nothing for them. Alongside complete it means a large project ran out of time; say which analyses are missing rather than presenting results as the whole picture, and note that a person can re-run to try for them (you cannot start a run). 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. results.components is NOT the whole device list: it is a bounded working set — the devices the downstream analyses focused on. On complete the response carries devices: { total, unclassified, identified, href } for the full inventory (identified is absent on older runs, not 0); page through it with plcs_get_project_devices when asked about a device that is not in results.components, or for a count of everything. Requires the analysis_tab permission.

Inputs

InputTypeDescription
id requiredstringThe project id.
version_idstringRead 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.

plcs_get_project_devices

Page through the project's device inventory

GET/projects/{id}/devices
Read-onlyRequires analysis_tab

Page through EVERY device the analysis found in a project — instruments, drives, I/O points, controller links — as the analysis stored them. This is the full inventory; the components inside plcs_get_project_analysis are a bounded working set selected out of it, so a device missing there is usually here. A read (no charge). Check status first: stored — devices is a page, total and identified count the whole set matching your filters, and you follow next_cursor (pass it back as cursor) until it is null; absent — the version was analyzed but no inventory is stored for it, a normal empty answer, not an error; not_analyzed — nothing has been analyzed for this version and nothing you can call will start one (see plcs_get_project_analysis). Each row carries identified: whether something NAMED the device (a catalog number, an instrument code, the analysis) rather than merely finding a wire on it — identified: false with a typed type is a wire whose direction is known and whose device is not. Filter with identified=true for the named ones, type for one device type (e.g. vfd, unknown), module for one I/O drop. Do not change filters between pages: the cursor is bound to the filter set and the version it was issued for. Default page is 200 rows, max 500 — a large plant runs to thousands, so summarize rather than reading every page unless asked. version_id reads an older version's inventory; such a response carries is_current_version: false and describes code the project no longer contains — say so. Requires the analysis_tab permission.

Inputs

InputTypeDescription
id requiredstringThe project id.
version_idstringList this version's inventory instead of the current version's. Must be a version of the project in the path.
limitintegerPage size (default 200, max 500).
Default 200 · min 1, max 500
cursorstringOpaque cursor from a previous response's next_cursor.
typestringOnly devices whose type equals this (e.g. vfd, digital_input, unknown).
modulestringOnly devices wired to this I/O drop (location.module).
identifiedbooleantrue → only identified devices; false → only the rest.

Save

plcs_save_version

Commit a new version (Save project)

POST/projects/{id}/versions
WriteConsequentialIdempotent retryRequires code_write

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_upload_id (from plcs_stage_upload). The server picks inline-vs-direct-upload 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 with this project id, PUT the bytes to the target it returns, then pass its file_upload_id + 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

InputTypeDescription
id requiredstringThe project id.
namestringOptional display name for the project.
idempotency_keystringOptional 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.
originalFilenamestringThe vendor file name (e.g. "Injection_Molding.L5X" or "Reference_Coiler.zip"). Optional when using file_path — derived from the path basename.
file_pathstringPath to the vendor file on the machine running this MCP server (on-prem/local mode). The server reads it and handles upload sizing.
file_inlinestringBase64-encoded file bytes — an alternative to file_path when the file is not on the server's filesystem. Large files are routed to the direct-upload flow automatically.
file_upload_idstringThe file_upload_id from plcs_stage_upload, once the file bytes have been PUT to its target. Provide originalFilename alongside it, and the same project id the upload was staged for.

plcs_stage_upload

Stage a file upload (token-free)

WriteMCP server toolRequires code_write

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 target: PUT the raw file bytes there over plain HTTP with exactly the returned headers (a ready curl command is included) so the bytes never pass through the model. Then call plcs_save_version with the same project id, the returned file_upload_id, and originalFilename. The target accepts one file, once, for that project only, and is short-lived. This creates NO project version on its own, is NOT billable, and makes no change to any project.

Inputs

InputTypeDescription
id requiredstringThe project the file will be saved to (the same id you pass to plcs_save_version).
filename requiredstringThe 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

POST/projects/{id}/exports/plc
WriteIdempotent retryRequires export_plc

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

InputTypeDescription
id requiredstringThe project id.
idempotency_keystringOptional 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

POST/projects/{id}/exports/pdf
WriteIdempotent retryRequires export_pdf

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. detail picks the tier: project (the default — structure, tags and analysis, no logic drawings or code; prints for every project), diagrams (adds the ladder/FBD/SFC drawings) or everything (adds the source listings too). Pass a larger tier ONLY when the human asks for drawings or code: on a large plant those tiers are refused — the job ends with error: "pdf_too_large" — while project prints. The Components Analysis chapter lists the analysis roster by default; pass full_device_inventory: true ONLY when the human asks for every device in the report — it prints the whole stored inventory (the rows plcs_get_project_devices pages) at about 23 devices per page, so a large plant adds hundreds of pages; no detail tier turns it on. Requires the export_pdf permission.

Inputs

InputTypeDescription
id requiredstringThe project id.
detailstring enumHow much logic the report carries. project: structure, tags and analysis, no drawings or listings — prints for every project. diagrams: adds the logic drawings. everything: adds the source listings as well; the output this operation always rendered before this field existed. The two larger levels are refused as pdf_too_large on a large plant.
Values: project diagrams everything · Default project
full_device_inventorybooleanList every device in the version's stored inventory in the Components Analysis chapter, instead of the analysis roster. About 23 devices per page.
Default false
idempotency_keystringOptional 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

GET/exports/{exportId}
Read-onlyRequires export_plc / export_pdf

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

InputTypeDescription
exportId requiredstringThe export job id returned by startPlcExport / startPdfExport.

plcs_download_export

Download a completed export artifact

GET/exports/{exportId}/download
Read-onlyRequires export_plc / export_pdf

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

InputTypeDescription
exportId requiredstringThe export job id returned by startPlcExport / startPdfExport.

Embed

plcs_mint_embed_token

Mint a read-only embed token

POST/embed-tokens
WriteIdempotent retryRequires ai_explain

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

InputTypeDescription
project_id requiredstringThe project the embed token may access.
idempotency_keystringOptional 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.