API Integration Guide
How external systems call PlantPulse AI's MCP and API directly.
The old standalone services MCP Server(:50000) · Ontology(:8888) have been archived, and
:7114, which used to open LightRAG directly, no longer exists either. Tools are gathered at a single address, the MCP Gateway → MCP Tool Map
MCP clients connect through the single gateway address
External MCP clients (other agents · desktop apps, etc.) connect through one address, https://<호스트>/mcp/gateway/ (Streamable HTTP).
The tools of the platform's unified MCP · Time Series Insight · RAG · Document Understanding · Vision · Engineering Calculation · Digital Thread · external servers added by the operator are all gathered at this address.
| To do | Where |
|---|---|
| Get a key | Issue a key in AI Admin Console › MCP Gateway › Client API Keys — the key (ppgw_…) is shown only once, right after it is issued. Give the key a scope (equipment · site · project · customer PO · supplier) and a grade — a key without a scope can call only external server tools |
| Choose which tools it sees | Permissions in the same menu — a new key starts with nothing allowed. For each server, allow «All tools» or only some tools |
| Call | X-API-Key: ppgw_… or Authorization: Bearer ppgw_… |
| View calls | Monitoring › Calls & Metrics — calls are recorded per key |
- A missing or wrong key gets 401; a client that fails more than 20 times in one minute gets 429 for the rest of that minute.
- This path forwards only the gateway's MCP (
/mcp/). The gateway's management REST (/api/v1) is not exposed outside. /mcp/gatewaywithout the trailing slash is redirected to/mcp/gateway/(308).- Screens · permissions · key revocation → AI Admin Console — MCP Gateway
Other endpoints
| Service | URL | Protocol | Auth |
|---|---|---|---|
| MCP Gateway | https://<호스트>/mcp/gateway/ | MCP (Streamable HTTP) | gateway key ppgw_… — above |
| Copilot Public API | https://<호스트>/api/v1/… | REST | X-API-Key → below |
| LiteLLM (Models) | http://<server>:4000 | OpenAI compatible | master key |
| Unified MCP (Platform) | <platform>/api/v5/mcp | MCP (JSON-RPC 2.0) | api_key — Authorization: Bearer or X-API-Key |
Direct service access — for operators · system integration
The in-house MCP servers are also open directly, without going through the gateway. However, their authentication is a single installation-wide inbound key (PI_INBOUND_API_KEY),
so whoever holds that key has full rights, and there are no per-caller tool permissions and no call log.
Use it only for system integration such as operator scripts · platform CEP, and give customers' MCP clients the gateway.
| Service | Direct address | Auth |
|---|---|---|
| Time Series Insight | http://<server>:8970/mcp/ · https://<호스트>/mcp/timeseries/ | inbound key |
| RAG Gateway | http://<server>:7115/mcp/ · https://<호스트>/mcp/rag/ | inbound key |
| Document Understanding | https://<호스트>/mcp/parser/ (host port 7120 is not published) | inbound key |
| Vision | https://<호스트>/mcp/vision/ (host port 7130 is not published) | inbound key |
The only way to reach Digital Thread · Engineering Calculation · the platform's unified MCP at a single address is the gateway.
Also available as paths on the default host
Without opening separate subdomains or ports, the proxy forwards the following via paths on the default host (the same address as chat).
| Path | Forwards to |
|---|---|
https://<호스트>/mcp/gateway/ | MCP of the MCP Gateway — external MCP clients use this |
https://<호스트>/llm/ | LiteLLM — base_url in the OpenAI SDK is https://<호스트>/llm/v1 |
https://<호스트>/mcp/rag/ | MCP of the RAG gateway |
https://<호스트>/mcp/timeseries/ | MCP of Time Series Insight |
https://<호스트>/mcp/parser/ | MCP of Document Understanding |
https://<호스트>/mcp/vision/ | MCP of Vision |
Authentication is done by the back end — /mcp/gateway/ needs the gateway key ppgw_…, the direct MCP mesh needs X-API-Key: <PI_INBOUND_API_KEY>, and /llm/ returns
401 without the LiteLLM master key. Since the default host is typically an address
with external DNS attached, be sure to check after installation that no key is left
exposed via CHANGE_ME_… → Credentials Management
The LiteLLM admin screen (/llm/ui) does not open via this path — open it via :4000.
Inbound key on the back end
Time Series Insight · RAG · Document Understanding · Vision · Digital Thread all use
the same inbound key (PI_INBOUND_API_KEY in the installation env) →
Credentials Management. Send it as X-API-Key: <key> or Authorization: Bearer <key>.
-
Only
/ping·/healthopen without a key./metricsalso requires a key. -
Missing or wrong key results in 401 (RAG uses
missing or invalid API key). -
🔴 If the key is empty, the service will not start (fail-closed). The installation check (
bin/lint-env.sh) stops first with[fail] PI_INBOUND_API_KEY is empty and PI_INBOUND_AUTH_DISABLED is not true, and the service log leavesFATAL: … is empty — refusing to start …. There is no "silently open back end" with an empty key. -
To deliberately turn off auth (e.g., an isolated test environment), set
PI_INBOUND_AUTH_DISABLED=truein the installation env (defaultfalse). compose passes this value under a per-service name.Service Name the container reads RAG RAG_INBOUND_AUTH_DISABLEDTime Series Insight TS_INBOUND_AUTH_DISABLEDDigital Thread DT_INBOUND_AUTH_DISABLEDDocument Understanding · Vision INBOUND_AUTH_DISABLEDIf a key is set, the key wins (lint-env reports this via
[note] … is ignored — PI_INBOUND_API_KEY is set and the key wins). Values outsidetrue·falseare[fail]. A service that started with auth off records this as such in/health(Digital Threadauth.inbound=disabled, Time Series Insightinbound_auth: "disabled") — running Run Check on the API Keys screen of the admin console catches it as open → AI Admin Console — API Keys
Document Understanding·Vision do not publish host ports (7120 · 7130) — from outside
they are reachable only via the /mcp/parser/ · /mcp/vision/ paths above. Digital Thread (7140) also
does not open a host port.
It's /mcp/, not /mcp. Omitting the slash causes a redirect or a 404.
Calling the MCP protocol
All five servers follow the MCP (Model Context Protocol) standard. The plant
data·ontology tools are on the platform's unified MCP (/api/v5/mcp), waveform analysis is
on Time Series Insight, and documents·images are on the remaining three.
When calling a server directly, use the unprefixed name (analyze_asset). Prefixes like
insight_ are attached by chat's tool registrar, so the name you see on the chat screen
differs from the name used here.
Basic MCP structure
MCP is HTTP POST communication based on JSON-RPC 2.0. The unified MCP requires
api_key authentication (Authorization: Bearer <token>).
POST /api/v5/mcp HTTP/1.1
Content-Type: application/json
Authorization: Bearer <platform.api.token>
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "<tool-name>",
"arguments": {
"<param1>": "<value1>",
"<param2>": "<value2>"
}
}
}
Retrieving the tool list
curl -X POST <platform>/api/v5/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <platform.api.token>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list"
}'
Tool call example
search_domainsPeople speak in names; tools take IDs. The default order is for search_domains to do a
partial match across 14 domains to pin down "domain + ID," then pass that ID to the
detail tool.
① Look up by name to get the ID
curl -X POST <platform>/api/v5/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <platform.api.token>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "search_domains",
"arguments": {
"query": "압축기",
"domain": "asset",
"limit": 20
}
}
}'
If you know domain, specify it for a precise search; if not, omitting it splits results
by domain.
② Expand the asset hierarchy
curl -X POST <platform>/api/v5/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <platform.api.token>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_asset_tree",
"arguments": { "asset_id": "<①에서 얻은 ID>" }
}
}'
tags key does not mean "no tags"By default only the count (tag_count) comes with the tag. Pass include_tags: true only when you
actually need the name·tag_id — the response grows several times larger. The source
of truth for tag presence is tag_count, and only tag_count: 0 means genuinely no tags.
③ Read tag current values at once
curl -X POST <platform>/api/v5/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <platform.api.token>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_tag_values",
"arguments": { "tag_ids": ["<tag_id>", "<tag_id>"] }
}
}'
tag_ids is an array, and up to 100 at once can be included. In the response,
found: false means an unregistered tag, and error means a lookup failure (worth retrying).
Historical period lookup is get_tag_history.
Unified MCP Authentication
The unified MCP requires api_key authentication. The value chat uses is Copilot's
platform.api.url · platform.api.token (environment variables CO_PLATFORM_API_URL ·
CO_PLATFORM_API_TOKEN — compose moves PP_API_URL · PP_API_TOKEN from the host env into these). The prefix
CH_ used through 2026.09 changed to CO_ in 2026.10, with no fallback.
⚠ The old names mcp.api.url · mcp.api.token no longer exist. Old config rows are migrated
to the new names at startup, but if you set the environment variable under the old
name, nothing happens.
Auth header
curl -X POST <platform>/api/v5/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <platform.api.token>" \
-d '{ ... }'
Calling the ontology (knowledge graph)
Ontology tools have been absorbed into the unified MCP. 🔴 There is no tool named
ontology_* — that was the name from the old standalone service (:8888) days. There are
now three.
| Tool | What it answers |
|---|---|
get_ontology | Configuration overview of one site — zone/line/equipment structure + tag count per equipment |
query_ontology | Directional relationship search — "what does X affect," upstream·downstream |
query_sparql | Multi-condition cross query (SELECT / ASK only) |
The names are similar but return different axes. For pure hierarchy structure use
get_asset_tree, for site configuration overview use get_ontology, and for relationship
propagation use query_ontology.
Site configuration overview
curl -X POST <platform>/api/v5/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <platform.api.token>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_ontology",
"arguments": { "site_id": "<site_id>" }
}
}'
site_id is required — resolve it first via domain: "site" on search_domains.
total_tag_count is the source of truth for total tag countDo not sum the per-node tag_count — unexpanded branches will be missing.
total_tag_count_unknown: true means "could not count," so do not estimate.
Relationship search — "what does this affect"
curl -X POST <platform>/api/v5/mcp \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <platform.api.token>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "query_ontology",
"arguments": {
"entity_id": "<설비 ID>",
"direction": "DOWN",
"depth": 2
}
}
}'
| Argument | Value | Meaning |
|---|---|---|
entity_id | (required) | The entity to start the search from |
relation_type | FEEDS · DEPENDS_ON · PRODUCES | All if omitted |
direction | UP · DOWN · BOTH | Default DOWN (downstream impact) |
depth | 1 ~ 5 | Default 2 |
The response is root + nodes[] + edges[] + truncated. If truncated: true, it was truncated
by depth or breadth, so narrow the scope and ask again.
Calling the Time Series Insight MCP
Equipment anomaly detection
curl -X POST http://127.0.0.1:8970/mcp/ \
-H "Content-Type: application/json" \
-H "X-API-Key: <PI_INBOUND_API_KEY>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "detect_asset_anomaly",
"arguments": {
"asset_id": "ASSET_DJ_M_01_1",
"lookback_minutes": 1440,
"top_k": 5
}
}
}'
If threshold is omitted, the default is 5.0. Sensor anomaly scores are not 0~1 but
a calibrated score (robust z relative to the baseline period), so setting 0
counts everything as anomalous even for a small rise above the baseline median →
AI Models
Equipment sensor prediction
curl -X POST http://127.0.0.1:8970/mcp/ \
-H "Content-Type: application/json" \
-H "X-API-Key: <PI_INBOUND_API_KEY>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "forecast_asset",
"arguments": {
"asset_id": "ASSET_DJ_M_01_1",
"prediction_length": 96,
"enable_bounds": true
}
}
}'
Equipment health score
curl -X POST http://127.0.0.1:8970/mcp/ \
-H "Content-Type: application/json" \
-H "X-API-Key: <PI_INBOUND_API_KEY>" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_asset_health",
"arguments": {
"asset_id": "ASSET_DJ_M_01_1"
}
}
}'
Calling RAG (LightRAG)
The RAG engine is LightRAG 1.5.7 (the image we've wrapped since 2026.10, pi/plantpulse-lightrag),
but 🔴 it is not called directly. LightRAG lives only inside the container, and
the only externally reachable spot is the rag-api(7115) gateway alone.
Document upload·listing·deletion are done in the admin console (the old LightRAG
WebUI has been absorbed into the console). Search is called via the gateway's REST,
authenticated with X-API-Key.
Document search in copilot chat calls this gateway's MCP tool rag_search through the MCP Gateway (the copilot has
no built-in document search). REST /api/v1/query and MCP rag_search return the same envelope — neither attaches to LightRAG directly. The response shape below is not LightRAG's but
the envelope defined by the gateway.
Text search
curl -X POST http://127.0.0.1:7115/api/v1/query \
-H "Content-Type: application/json" \
-H "X-API-Key: <PI_INBOUND_API_KEY>" \
-d '{
"query": "펌프 정비 절차",
"mode": "hybrid"
}'
| Argument | Value |
|---|---|
query | Search query — 3 characters or more (shorter returns 400) |
mode | hybrid (default) · naive · local · global — any other value returns 422 |
| Response | Meaning |
|---|---|
{"retrieved": true, "answer": …, "sources": [{"file_name": …, "reference_id": …}]} | Found. reference_id is the n in the source marker [n] in the answer body (absent for sources the engine did not number) |
{"retrieved": false, "code": …, "reason": …, "sources": []} | Not found. The answer key itself is absent |
| HTTP 502 | Engine-side failure |
retrieved: false is not an error but "nothing found"HTTP is 200. If code is NO_CONTEXT, there's no relevant content in the indexed
documents; if EMPTY_ANSWER, the engine returned empty-handed. Calling again returns the
same answer — do not display a response without answer as though it were a
result.
Drawing comparison — differences between two revisions
Usable on installations with drawing collection (DRAWING_INGEST_ENABLED=true) turned on →
Configuring Environment Variables. There are two entry points,
and the comparison itself is done by Document Understanding. rag-api pulls the
originals of the two indexed revisions and hands them to Document Understanding,
returning its result as-is. Both require the inbound key.
| Entry point | When |
|---|---|
rag-api GET /api/v1/drawings/{asset_id}/{doc_name}/diff | Already indexed drawings — finds the prior revision and compares |
Document Understanding POST /api/v1/drawings/diff | Compares two files on hand directly (regardless of indexing) |
Comparing against the prior revision of an indexed drawing (rag-api)
curl -G "http://127.0.0.1:7115/api/v1/drawings/<asset_id>/<doc_name>/diff" \
-H "X-API-Key: <PI_INBOUND_API_KEY>" \
--data-urlencode "lang=ko"
| Argument | Meaning |
|---|---|
against | The document name of the prior revision to compare against. If blank, picks the one whose revision marker is immediately prior among the same drawing number (A < B < … < Z < AA, 1 < 2 < 10) |
against_asset | The equipment ID if the prior revision is attached to a different equipment (default: same equipment) |
lang | Language for the summary line, ko · en. If blank, installation language (PP_LANG). Numbers are as written on the drawing regardless of language |
| Status | Meaning |
|---|---|
404 | Either side is not indexed, the original is not archived, or there's no prior revision to pick |
422 | The comparator could not read the file |
502 | Could not reach the comparator or object storage |
The same router has page previews (…/page/{n} — PNG for PDF, SVG for DXF) and revision
status (…/revision-status — current · outdated · change_open · unknown).
Comparing two files directly (Document Understanding)
Since Document Understanding does not publish a host port, call it externally via the
parser.* host.
curl -X POST "https://parser.<domain>/api/v1/drawings/diff" \
-H "X-API-Key: <PI_INBOUND_API_KEY>" \
-F "old=@A-100_rev-A.dxf" -F "new=@A-100_rev-B.dxf" -F "lang=ko"
- Files may be
.dxfor vector.pdfwith a text layer, up to 50 MB each. DWG is not accepted. - The default for
langisen(differs from the rag-api entry point). - Rejections come with
error_kind: input, for extension415· size413· unreadable400.
Shape of the result
| Key | Contains |
|---|---|
title | Changed title block fields |
dimensions | added · removed · changed (which of value·unit·count·tolerance·text·position changed) · unchanged |
bom · notes | Changed BOM rows · notes |
regions | Changed regions — bbox in the later revision and bbox_old in the prior revision. If alignment fails, status: unavailable with the reason, and regions are not fabricated |
summary | Human-readable summary line — numbers exactly as written on the drawing |
confidence · limits | high · medium · low, and things that could not be compared (scanned page · no title block · BOM only on one side, etc.) |
warnings | different_drawing_numbers (drawing number differs) · revision_label_unchanged (same revision marker), etc. |
Video · Audio — /api/v1/media of rag-api
This path exists only on installations with video·audio collection turned on
(installation env PI_MEDIA_ENABLED=true). On installations where it's off, this path doesn't exist
at all, returning 404, and video·audio upload is rejected with 400. Whether
it's on is checked via media_ingest_enabled of GET /api/v1/config →
Configuring Environment Variables ·
AI Admin Console — Video
| Method · Path | What it does |
|---|---|
GET /api/v1/media/jobs | Per-file processing status · duration · segment count · number of scene stills (thumb_count) and processing time bands (window — open · next_start · spec) |
GET /api/v1/media/jobs/{asset_id}/{doc_name} | Details of a single file |
GET /api/v1/media/integrity | Segments that are indexed but missing from the search index |
POST /api/v1/media/jobs/{asset_id}/{doc_name}/retry | Re-queue (failed · skipped). If ?force=true, transcribe again from scratch |
GET /api/v1/media/{asset_id}/{doc_name}/transcript | The archived transcript |
GET /api/v1/media/{asset_id}/{doc_name}/thumb?t=<ms> | The nearest scene still before that time |
GET /api/v1/media/{asset_id}/{doc_name}/stream | Playback — responds with 206 to a Range request |
- Uploading uses the same
POST /api/v1/docs/uploadas documents, and deletion uses the sameDELETE /api/v1/docs/{asset_id}/{doc_name}(what comes from the platform ledger is409). - Scene stills (
…/thumb) carryETagandCache-Control: private, max-age=86400. Sending the sameETagasIf-None-Matchreturns 304 without re-reading the still. If the state DB is briefly busy, it returns 503 withstate database busy, retry shortlyandRetry-After: 1instead of 500 — call again after that time. - The relay used by the Copilot screen (Copilot's
/media/thumb·/media/stream, authenticated with login session) also passes throughETag· 304 as-is, and relays the back end's 503 as 503 withRetry-After(previously wrapped as 502).
Request id and metrics — X-Request-ID · /metrics
Starting from the 2026.10 re-release, the five Python back ends (RAG · Time Series Insight API · Document Understanding · Vision · Digital Thread) follow the same rule.
- Request id — If you send
X-Request-ID(1~128 chars, letters·digits·._-) on the request, that value is returned in the response headerX-Request-ID; if missing or malformed, a newly generated value is returned instead. The same value is attached asrequest_idto log lines that occur while that request is running. RAG also addsrequest_idto error response bodies. - Metrics —
GET /metricsemits Prometheus text format. It requires the inbound key (unlike the health path, it is not exempt). The port has not changed → Logs and Monitoring
curl -s -D - -o /dev/null -H "X-Request-ID: my-trace-001" \
-H "X-API-Key: <PI_INBOUND_API_KEY>" http://127.0.0.1:7115/api/v1/config | grep -i x-request-id
Calling the LLM API (OpenAI compatible)
The LiteLLM proxy provides an OpenAI-compatible API. You can use the existing
OpenAI SDK as-is.
On the LAN it's http://<server>:4000/v1, and via the default host it's https://<호스트>/llm/v1.
Chat completion
curl http://127.0.0.1:4000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 설치-시-변경" \
-d '{
"model": "aura-lite",
"messages": [
{"role": "user", "content": "주입기 베어링 교체 주기를 알려줘."}
],
"temperature": 0.7
}'
Generating embeddings
curl http://127.0.0.1:4000/v1/embeddings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 설치-시-변경" \
-d '{
"model": "aura-embedding-4b",
"input": "주입기 베어링 교체 절차"
}'
Checking the model list
curl http://127.0.0.1:4000/v1/models \
-H "Authorization: Bearer 설치-시-변경"
Available models
| Model name | Purpose | Actual model |
|---|---|---|
aura-lite | Conversation · analysis (also handles image input) | Aura — default Aura-Lite-NVFP4 |
aura-vision | Vision-only alias of the same engine as above | Same model as above |
aura-embedding-4b | Embeddings | Aura-Embedding-4B |
aura-reranker-0.6b | Reranking | Aura-Reranker-0.6B |
gpt-4o · text-embedding-3-large now return an errorOlder versions accepted the OpenAI-compatible aliases (gpt-4o · gpt-4o-mini · text-embedding-3-large ·
rerank-multilingual-v3.0). They are no longer accepted — if integration code still has the old
names, the call is rejected. Switch to the names in the table above. You can check
the currently live names via /v1/models.
Whichever name is used, it routes to the Aura model running in-house — the same applies to air-gapped network installations.
Python SDK example
Calling the LLM via the OpenAI SDK
from openai import OpenAI
client = OpenAI(
base_url="http://127.0.0.1:4000/v1",
api_key="설치-시-변경"
)
response = client.chat.completions.create(
model="aura-lite",
messages=[
{"role": "user", "content": "주입기 상태를 분석해줘."}
]
)
print(response.choices[0].message.content)
Calling MCP tools
import requests
def call_mcp_tool(url, tool_name, arguments=None, headers=None):
response = requests.post(
url,
headers=headers or {},
json={
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": tool_name,
"arguments": arguments or {}
}
}
)
return response.json()
# 이름으로 설비를 찾는다 (통합 MCP — api_key 필요)
result = call_mcp_tool(
"<platform>/api/v5/mcp",
"search_domains",
{"query": "압축기", "domain": "asset"},
headers={"Authorization": "Bearer <platform.api.token>"}
)
print(result)
# 설비 이상 탐지 (TimeSeries-Insight — 인바운드 키 필요)
result = call_mcp_tool(
"http://127.0.0.1:8970/mcp/",
"detect_asset_anomaly",
{"asset_id": "ASSET_DJ_M_01_1", "lookback_minutes": 1440},
headers={"X-API-Key": "<PI_INBOUND_API_KEY>"}
)
print(result)
Copilot Public API (/api/v1)
The entry point for external systems to use chat and morning cards as-is. It lives at the same address as chat (the default host).
| Method · Path | What it does |
|---|---|
POST /api/v1/chat | Ask a question and receive an answer. The server picks whichever tools are needed |
GET /api/v1/tasks/daily | Read the day's daily card (generated daily at 05:00). ?date=YYYY-MM-DD — based on the plant time zone; if blank, today |
GET /api/v1/tools | The list of tools the agent can use right now (name·description·read-only flag) |
GET /api/v1/ai-feedback | Read markings people have put on AI results (false alarm·suggestion accepted/rejected/effect) and markings the server has made by rule (correct·false alarm·missed) — 2026.10 (below) |
curl -X POST https://<호스트>/api/v1/chat \
-H "Content-Type: application/json" \
-H "X-API-Key: <발급받은 키>" \
-d '{ "input_value": "지금 끊어진 엣지 장비가 몇 대야?" }'
The response is answer (markdown) · session_id · duration_ms · tools (the tools called
while producing this answer). Omitting or leaving session_id blank opens a new
conversation (the same as pressing "New Conversation" on screen — default app,
owned by the caller), and that id is returned as session_id in the response. To continue
a conversation, include that value in the next request. Since this is a synchronous
call, questions that call tools multiple times can take tens of seconds.
session_id was a 500The docs said "blank opens a new conversation," but the server returned 500, and calls
made with X-API-Key also ended in 500. Both were fixed in 2026.10.
From the 2026.10 re-release 3 onward, if the model still fails to produce an answer
after retrying, it returns HTTP 502. The body shape is the same, with answer
left empty.
This prevents an empty answer from being received as 200 and displayed as though it
were a result — just resend the same question.
Human-applied markings on AI results — GET /api/v1/ai-feedback
An entry point for reading only the markings people apply to AI results (dismissal
reason·accept/reject·effect) within Copilot's Suggestions (the only way to write
them is through the screen) →
AI Findings and Accuracy — Human-Applied Judgments.
Authentication is the same as other /api/v1 (X-API-Key only — from 2026.10 re-release 6,
login sessions return 403).
curl "https://<호스트>/api/v1/ai-feedback?since=2026-10-01T00:00:00Z&limit=500" \
-H "X-API-Key: <발급받은 키>"
| Argument | Meaning |
|---|---|
since | labeled_at lower bound (RFC 3339 or epoch ms). If blank, from the beginning |
asset_id | Filter by equipment id |
target_kind | ANOMALY (one anomaly) · FINDING (AI finding) · PROPOSAL (suggestion card from a finding) · MISSED (missed-case report) · DAILY_TASK |
limit | Max line count 1~1000, default 500 |
The response is count · items · next_since. Since results come oldest first, call
the next page with next_since — lines at the same timestamp can overlap, so filter
with feedback_id. If a person presses again on the same target, that line's value ·
labeled_at changes (the last marking wins).
label_key | value |
|---|---|
verdict | hit · false_alarm · unsure (judgment withheld — excluded from accuracy calculation) |
reason | False alarm reasons setup · idle · maintenance · sensor · other / suggestion rejection reasons already_done · not_needed · wrong · other |
cause | Actual cause breakdown · degradation · sensor · setup · maintenance · normal · other |
proposal_decision | accepted · rejected |
outcome | effective · not_effective · unknown (memo in comment) |
missed | missed (explanation in comment, missed incident time in occurred_at) |
The values people leave on the new screen (the three-menu set) are verdict's false_alarm
(dismissal of an anomaly to check) and its reason, proposal_decision, outcome. hit ·
missed are assigned by the server's stop·fault rule matching, while unsure ·
cause are in lines left from the old screen.
Each line comes together with occurred_at (the incident time the marking refers to) ·
labeled_by · labeled_at · config_ver (the judgment version that produced that result), and
…Stamp next to the time is written in the plant time zone (PP_TZ).
Keys — two methods
| Method | Where | Nature |
|---|---|---|
| Issued key (recommended) | Copilot Admin Console → API Keys → Issue | Issue multiple named keys and revoke individually. Last-used time is tracked |
| Environment variable key | CO_API_V1_KEY in /etc/kopens/plantpulse-ai.env (CH_API_V1_KEY through 2026.09 — the old name is silently ignored) | Only one line. Leaving it blank turns off authentication via this key only |
Since it is stored only as a hash, the list shows only the leading characters. If lost, issue a new one and revoke the old.
The authority this key grants is /api/v1 and the Copilot MCP (/mcp). From 2026.10
re-release 6, these two paths open only via key, and calling with a logged-in
browser session returns 403. The permission·document classification of calls made with
this key follow the "external API" role bundle (default: confidential), and scope is
always full → Access Control.
The admin path and API docs screen (/swagger-ui.html) require admin login. There is no open
path to call tools "directly" — tools that change state carry a human approval step.
Changes to APIs used by the screen (2026.10 re-release 3)
The following are not public APIs but paths the Copilot screen calls with a login session. If you have integrations that mimic the screen, check these.
| Method · Path | What changed |
|---|---|
PUT /api/employees/{아이디}/work-role | 🔴 Now admin only (previously also allowed for oneself). Body {"workRole": "maintenance"}. Each change is left in the audit log as employee.work_role:<아이디> → User Permissions and Work Roles |
PUT /api/employees/{아이디}/home-role | New path — one's own start screen preference. Body {"homeRole": "manager"} (same five values as work roles). Called by oneself or an admin. Does not change approval authority. Since the new screen (the ask·suggestions·automation menu set) always opens to "ask" for everyone, it doesn't use this value — the path remains |
GET /api/sop?query=…&assetId=… | Gained the assetId argument — when set, excludes sources from other equipment. The response is {answer, sources[], citations, truncated}, and the answer is truncated at a sentence boundary within 600 characters (truncated). Too-short query returns 400; if the document search service doesn't respond, 503 + Retry-After |
Response format
MCP success response
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{ ... 도구 실행 결과 JSON ... }"
}
]
}
}
MCP error response
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32600,
"message": "Invalid request",
"data": "상세 오류 메시지"
}
}
When volume is large — limit and has_more
🔴 There is no page · size. Unified MCP tools set an upper bound via
limit, and the response signals "there's more" via has_more.
{
"name": "search_domains",
"arguments": {
"query": "압축기",
"domain": "asset",
"limit": 50
}
}
| Tool | limit default | Max |
|---|---|---|
search_domains | 50 | 500 |
get_tag_history | 500 | 500 |
get_tag_values | (array tag_ids — at once) | 100 items |
has_more: trueSince there are no page numbers, narrowing the condition is the standard approach
— specify domain, cut the time range, or make the search term more specific. It is
not a form where you keep pushing the same query to get the next page.
Time format
Time parameters of the unified MCP are server local time. The names are from ·
to, and the format is YYYY-MM-DDTHH:MM:SS.
{
"name": "get_tag_history",
"arguments": {
"tag_id": "<tag_id>",
"from": "2026-03-29T00:00:00",
"to": "2026-03-29T23:59:59"
}
}
Z is rejected2026-03-29T00:00:00+09:00 or ...Z are not accepted. Only offset-free local time is accepted —
check what time the server currently thinks it is via get_current_time.
There are also no names start_time · end_time. They are from · to.
Fields suffixed with _iso in the response are in human-readable KST format:
{
"timestamp": 1711641600000,
"timestamp_iso": "2026-03-29 09:00:00"
}