A side-by-side look at the Forge MCP and Kilo IoT Server MCP servers: transport, auth, tools, quality signals, and the config to install either one in Claude, Cursor, Windsurf or VS Code.
Verdict: choose Forge MCP for industrial & iot work over a hosted endpoint; choose Kilo IoT Server for industrial & iot over a hosted endpoint. You can also run both side by side in one mcpServers block.
FOUNDRYNET_API_KEY, FORGE_BASE_URL, PORT, REQUEST_TIMEOUT.identify_machine, normalize_telemetry, query_machine_history.Rows where the two servers differ are marked.
| Spec | ||
|---|---|---|
| Category | Industrial & IoT | Industrial & IoT |
| Quality signal | 59/100 · Good | 37/100 · Fair |
| Install command | Confirmed | Confirmed |
| Transport | Remote HTTP/SSE | Remote HTTP/SSE |
| Auth | API key required | Not declared |
| Pricing | Freemium | Not declared |
| Required env vars | FOUNDRYNET_API_KEYFORGE_BASE_URLPORTREQUEST_TIMEOUT | None listed |
| Verified owner | Community listing | Community listing |
| GitHub stars | 0 | 0 |
| Directory activity | 3 views 0 installs 0 upvotes | 1 views 0 installs 0 upvotes |
| Tools | 32 | Not listed |
| Full listing | View Forge MCP | View Kilo IoT Server |
identify_machineProvision or retrieve a persistent identity (mint_id) for any
industrial machine. Works for CNC machines, industrial robots, PLCs,
additive manufacturing cells, injection molders, presses, turbines,
pumps, compressors, conveyors — any equipment from any OEM:
Fanuc, Siemens, Haas, DMG Mori, Mazak, Okuma, Hurco, Doosan, Makino,
ABB, KUKA, Universal Robots, Yaskawa, Stäubli, FANUC Robotics,
Komatsu, Caterpillar, John Deere, Trumpf, Bystronic, Amada, EMAG,
Bosch Rexroth, Beckhoff, Rockwell Allen-Bradley.
Returns the mint_id (universal handle, format "MINT-xxxxxx"). Idempotent
— calling again with the same (oem, model, serial) returns the same
mint_id with `created: false`.
USE WHEN: a user references a specific machine by OEM/model/serial and
you need a stable handle to attach normalized data, automations, or
tamper-evident work records to. Always call this first when a new machine is
introduced to the conversation, before normalize_telemetry or
create_automation.normalize_telemetryGive your agent a semantic understanding of machine data from any OEM:
translate raw vendor telemetry into one universal canonical schema (FCS,
FoundryNet Canonical Schema) so the agent can reason across vendors it has
never seen before.
Maps vendor-specific column names like "Spindle_Speed", "servo_load_x",
"CoolantTemp", "FeedRateOverride" into standard fields like
spindle_speed_rpm, axes.x_load_pct, sensor_readings.coolant_temp,
feed_override_pct.
Accepts a `data` dict of {raw_field: value}. If `machine_id` (mint_id
or internal_id) is omitted but oem+model+serial are provided, silently
auto-provisions the machine identity (same effect as calling
identify_machine first).
Each call:
- Returns canonical_data + a per-field mapping_id (use mapping_id
with /v1/feedback/{mapping_id}/correct if a mapping is wrong)
- Writes a row to forge_normalized_history (visible via
query_machine_history)
- Evaluates active triggers; the response includes a `triggers_fired`
array if any condition matched. The actual webhooks fire async, so
the array tells you what was triggered without blocking on remote
latency.
USE WHEN: you have raw machine data — a CSV row, a sensor reading, an
MES export, an alarm log line — and need to either (a) understand it
semantically using canonical field names, (b) feed an automation that
watches canonical fields, or (c) build up history for the machine.query_machine_historyRetrieve operational history for an identified machine. Each row is
one /v1/normalize call's canonical output (FCS field → value).
Query options:
from_dt, to_dt ISO-8601 timestamps to bound the time range
fields comma-separated FCS field names to project; omit for
full canonical_data
limit max rows (1–1000, default 100)
summary true → returns aggregate stats only (row_count,
time range, avg coverage_pct, fields_covered set)
without the raw rows. Always cheap.
USE WHEN: your agent needs to reason over how a machine has been running,
surface utilization or throughput or health trends, find patterns in
alarms or operational state, compare periods ("how was today vs
yesterday"), or discover what data is even available for a machine. Prefer
`summary=true` first to orient on volume + which fields are present,
then drill in with field projection on a smaller time window.create_automationLet your agent wire machine telemetry to any business system in plain
English — ERP, CMMS, MES, Slack, Teams, email, Zapier, n8n — via webhooks
already registered as tools on the Forge service. The agent describes the
condition and the action; Forge parses it into a structured trigger.
Examples of `instruction`:
"Alert maintenance Slack when spindle load exceeds 90 percent."
"Create a Fiix work order when coolant temperature stays above 35°C
for five minutes."
"Notify the supervisor when part_count hits 500."
"When the maintenance_type changes to CORRECTIVE, post to the ops
channel."
Returns a `parsed_trigger` JSON for HUMAN review — DOES NOT
auto-activate. The caller (you, with user confirmation) must
explicitly POST the parsed_trigger to /v1/triggers on the Forge API
to actually create it. The response includes
`confirmation_required: true` and may include `notes` if the parser
had to make a fuzzy match (e.g. resolved an ambiguous field name to
its closest canonical match).
USE WHEN: a user wants to set up monitoring, alerts, or automations
for machine state transitions. Always show the parsed_trigger to the
user verbatim and ask "Confirm to activate?" before they activate it.activate_automationActivate a parsed automation trigger on a machine. Call this AFTER
create_automation returns a parsed_trigger and the user explicitly
confirms they want to arm it.
Creates a live trigger that monitors the machine's normalized telemetry
and fires the listed actions when the condition matches. Each action
references a registered tool by tool_id; on fire, the tool's webhook
is POSTed with {{variable}} interpolation against the canonical data
context (mint_id, oem, model, serial, site, field, value, threshold,
plus every canonical field on the matched record).
Inputs:
machine_id mint_id ("MINT-…") or internal_id; resolved to canonical mint_id
name short human label, ≤ 80 chars (e.g. "high spindle load")
condition simple {field, op, value|threshold} OR compound {all: [...]}
ops: >, <, >=, <=, ==, !=
actions list of {tool_id, payload_overrides?, headers_overrides?}
enabled defaults to true; pass false to create the trigger paused
Returns the persisted trigger row including `id` (use it later to
pause/edit/delete via the Forge API). Once active, the trigger fires
on every subsequent normalize_telemetry call where the condition
matches — no further activation needed.
USE WHEN: the user has reviewed the parsed_trigger from
create_automation and said something like "yes, activate it" /
"go ahead" / "arm it." Never call this tool without explicit
confirmation — it changes machine behavior in a way the user can
feel (real Slack messages, real ERP work orders).list_automationsList all active automations / triggers configured for one machine.
Returns each trigger with: id, name, condition (field/op/value or
compound `all`), actions (each resolved to its tool name + url +
method), enabled state, fire_count, last_fired_at, last_error.
USE WHEN: the user asks "what automations do I have on this machine"
/ "show me my triggers" / "what alerts am I getting" / "what's
monitoring this machine right now". Always pass the machine's
mint_id (or internal_id — both resolve).list_guardrailsList the hard safety guardrails configured on one machine.
Returns each guardrail with: id, name, the plain-English instruction it
was created from, its structured condition, scope, and enabled state.
Guardrails are NOT automations. An automation fires an alert and a human
decides; a guardrail blocks the action and nothing decides. You cannot
create, edit, disable, or override a guardrail through this MCP server —
that requires a human administrator. Attempting a write returns 403.
USE WHEN: before proposing or activating anything on a machine — "what
are the limits on this machine", "what am I not allowed to do here",
"why was my automation rejected". Reading this first is cheaper than
being rejected: pair it with check_guardrail to test a specific value.check_guardrailTest whether a proposed action would be blocked, WITHOUT attempting it.
`proposed` is a dict of canonical field -> value you are considering, e.g.
{"spindle_speed_rpm": 13000}.
TWO LAYERS ANSWER, AND ONLY ONE OF THEM ALWAYS EXISTS.
1. Registry-wide PHYSICAL BOUNDS. Always checked, nothing to configure.
These are possibility envelopes for the canonical field, not limits for
your machine: spindle_speed_rpm is declared [-50000, 50000] for every
machine in the fleet.
2. Per-machine GUARDRAILS written by an engineer. These carry the real
operating limits — and they are OPT-IN. A machine nobody has authored
rules for has NONE, and `list_guardrails` returns {"count": 0}.
So on a freshly provisioned machine:
check_guardrail(mid, {"spindle_speed_rpm": 13000})
-> {"would_block": false, "guardrail": null, "reason": null,
"violations": [], "guardrails_evaluated": 0,
"declared_bounds": {"spindle_speed_rpm": [-50000.0, 50000.0]},
"physical_bounds_checked": true}
`would_block: false` with `guardrails_evaluated: 0` does NOT mean 13000 rpm
is safe for that spindle — it means nobody has told Forge what the spindle's
limit is, and 13000 is not physically impossible. DO NOT read it as
permission. Check `guardrails_evaluated`; if it is 0, say so to the user
rather than acting on the absence of a rule.
Once a rule exists the shape is what you would expect:
-> {"would_block": true, "guardrail": "Spindle overspeed protection",
"reason": "spindle_speed_rpm 13000 > limit 12000", "violations": [...]}
A field that resolves to no canonical at all is REFUSED, not passed:
`unresolvable_fields` is returned and the action is blocked, because an
unplaceable field cannot be range-checked.
This is a dry run: it writes nothing and is not recorded as a blocked
attempt. Call it BEFORE acting so you can self-correct to a safe value
instead of being rejected. If would_block is true, do not retry the same
value — adjust to satisfy the stated limit, or tell the user that a human
administrator must change the guardrail.
USE WHEN: you are about to recommend or set a value on a machine and want
to confirm it is inside the safe envelope — and to find out whether an
envelope has been declared at all.disable_automationPause an automation trigger without deleting it. The trigger stops
evaluating against incoming /v1/normalize calls but its configuration
(condition, actions, history) is preserved. Re-enable later by
PATCHing /v1/triggers/{id} with `{"enabled": true}` (or by asking
the user to confirm and creating a follow-up tool for resume).
USE WHEN: the user wants to TEMPORARILY stop an automation — e.g.
"pause the high-spindle alert during planned maintenance," "stop
that alarm for now, I'll re-enable it tomorrow." Distinct from
delete_automation, which is permanent.delete_automationSoft-deletes the trigger (recoverable for 30 days via
restore_automation). The trigger immediately stops evaluating against
/v1/normalize calls and is hidden from list_automations, but the row
persists with deleted_at set so an accidental delete can be undone.
Use restore_automation to undo. For permanent deletion, the API
supports ?permanent=true.
Past forge_trigger_executions rows for this trigger remain in either
case (audit trail).
USE WHEN: the user wants to remove an automation they no longer need —
"delete the coolant alert," "remove that trigger." Safer than hard
delete because misclicks are recoverable; tell the user about
restore_automation if they later change their mind.query_webhook_historyShow webhook delivery history for a trigger — HTTP status codes,
response times, retry counts, errors. Use to verify webhooks are
actually delivering.
Returns up to `limit` most-recent execution rows (default 10, max 200),
each with: fired_at, http_status, attempt_count, response_time_ms,
error (if any), tool_name, target_url, and the settlement reference
(settled_tx) once the row has been rolled up via batch settle.
USE WHEN: a user asks "did the alert actually go out?" / "why didn't
Slack get pinged?" / "is the trigger working?" / "show me the last
few fires." Soft-deleted triggers can still be queried — useful for
forensic audits after a misclick + restore.restore_automationRestore a previously soft-deleted automation trigger within its 30-day
recovery window. Re-enables the trigger so it evaluates against
incoming /v1/normalize calls again.
Returns the restored trigger row plus `restored: true` and the
`restored_at` timestamp. 410 (Gone) if the trigger was deleted more
than 30 days ago and is past the restorable window. 409 if the
trigger isn't actually deleted.
USE WHEN: a user accidentally deleted a trigger and wants it back.
Also useful as the "undo" half of a "delete then change my mind"
flow — pair with disable_automation when the user wants to pause
rather than delete in the first place.No tool names declared yet. Check the project README from the listing page.
Paste either block (or both) into your client config, e.g. claude_desktop_config.json or ~/.cursor/mcp.json. We only show a config when the install command is confirmed.
{
"mcpServers": {
"foundrynet-forge-mcp": {
"url": "https://mcp.foundrynet.io/mcp"
}
}
}
{
"mcpServers": {
"kilo-iot-server": {
"url": "https://mcp-auth.kiloiot.io/mcp"
}
}
}