Configuration¶
All Artek-MCP settings live in one Ignition 8.3 config resource, com.artek.mcp/settings, which you edit on the module's settings page. Changes apply live: policy changes need no gateway restart.
The settings page¶
Open Home → Artek MCP → MCP Server (/app/artek-mcp) in the Gateway web interface. The page shows live diagnostics (version, uptime, counters, provider states) and edits every setting: policy toggles, rate, body and timeout limits, allowed origins, the mutation allowlist, provider enablement, API keys, tool profiles, database access and script endpoints.
- API keys are always shown masked (
****plus the last 4 characters). Keeping the mask keeps the token. Blanking it, or adding a key with an empty token, generates a new 64-hex token, shown once in the save confirmation. - Providers are listed with their live state. A provider whose key is absent from the settings is shown exactly as the gateway resolves it, so the checkbox and the live-state badge always agree.
- Database access lists the gateway's real database connections as checkboxes (name, vendor, status). A saved entry that matches no live connection is shown as its own row badged
MISSING, so it stays visible and removable. A*wildcard is shown as an Allow ALL connections toggle. See Database access. - Tool profiles has a Profile column in the API-keys table and a card listing the built-in profiles and editing custom ones. See Tool profiles.
- Saves are protected against conflicting edits. If two administrators edit at once, the second save is refused (409) instead of silently overwriting the first. A settings tab left open across a module restart also gets one 409 and needs a reload.
Editing the settings file¶
You can also edit the resource on disk:
<data>/config/resources/core/com.artek.mcp/settings/config.json
then trigger a config scan (POST /data/api/v1/scan/config with a gateway API key), or push it through the gateway's config API. Keys missing from the file fall back to the hardened defaults (readOnly defaults to true).
Applies live: API keys, readOnly, allowlists, rate limits, script-endpoint execution and provider enablement. Needs a gateway restart: route mounting, the tool timeout used by the projects and tags providers, and registering new script endpoints.
Mutation policy¶
Mutating tools pass two gates:
readOnly(defaulttrue) refuses every mutating tool.mutation.allowlistrestricts the targets a mutating call may touch. An empty list[]means deny every target;"*"is the explicit allow-all. When the list is not empty, every target of a call must be covered by an entry or the call is refused.
Entries are either:
provider:toolkeys, which allow the whole tool (for examplealarms:alarm_acknowledge), or- target paths, for example
[default]Mcp/for tag paths orMyProject/for project resources.
| Target kind | Target format |
|---|---|
| Tag | the canonical tag path, for example [default]Mcp/Setpoint |
| Project resource | <project>/<moduleId>/<typeId>/<path> |
| Config resource | config/<moduleId>/<typeId>/<path> |
| Perspective session | perspective/session/<sessionId> |
| Gateway REST route | api/<METHOD><path>, for example api/POST/data/api/v1/scan/projects |
| Logger level | logger/<name> |
| Users and roles | security/<userSource>/user/<username>, security/<userSource>/role/<role> |
| Gateway Network peer | gan/<serverName>/<tool> (the peer the call actually routes to) |
| Script endpoint | scripting/<project>/<name> (see Script endpoints) |
Boundary rules. An entry covers:
- its exact target (
config/ignition/database-connectionmatches only that resource); - anything below it across a
/boundary (config/ignition/database-connection/…); - when the entry itself ends in
/, anything under that prefix (api/GET/,[default]Mcp/).
A sibling that merely shares leading characters (config/ignition/database-connectionX) is not covered. Entries are not raw string prefixes.
A name is one segment. Where a target contains a name (a user source, user, role or Gateway Network peer), write a %, / or : inside the name as %25, %2F or %3A. Otherwise an entry for user bob would also cover a user named bob/ops. Names without those characters are written as they are.
A bare project type entry also reaches that type's root resource
page-config, stylesheet and session-props are single root resources of their type. An allowlist entry of the form <project>/<moduleId>/page-config (or stylesheet, session-props) with no trailing path authorizes writing that project's root resource. Before enabling writes, check the allowlist for entries matching ^[^:]+/[^/]+/(page-config|stylesheet|session-props)$.
Bare tool-family entries are flagged
An entry such as security/, config or api logs a WARN, because it also covers a project with that name.
Legacy allowlist reading¶
A settings file with no mutation.policyVersion was written under an older, permissive reading where [] meant unrestricted once readOnly was off. It keeps that reading until it is saved, so an upgrade cannot silently revoke a running gateway's write access. A write-enabled gateway in that state logs a WARN at load, and the settings page shows a LEGACY CONFIG banner with Allow ALL mutation targets pre-ticked to match. Save to record that choice explicitly, or untick it and name the targets to move to deny-by-default. Fresh installs and every saved configuration are deny-by-default.
Per-key mutation and database scoping¶
Tool profiles narrow which tools a key may call. Per-key scoping narrows what those tools may touch. A key can carry its own mutationAllowlist and dbConnections beside its profile:
{
"auth": {
"keys": [
{ "name": "mes-writer", "token": "...",
"mutationAllowlist": ["MesProject/"],
"dbConnections": ["MesDB"] },
{ "name": "reporter", "token": "...", "profile": "ops",
"mutationAllowlist": [] },
{ "name": "admin", "token": "..." }
]
},
"mutation": { "allowlist": ["MesProject/", "OtherProject/"], "policyVersion": 2 },
"db": { "connections": ["MesDB", "HistDB"] }
}
Here mes-writer may write only MesProject and query only MesDB, although the gateway permits more of both. reporter may change nothing. admin uses the gateway-wide lists unchanged.
Narrow only. A per-key list never grants; it only subtracts. A target must pass the gateway-wide list and the key's list, so the gateway-wide lists are always the upper bound for every key, and you can reason about a gateway's exposure without reading every key. Giving a key an entry the gateway-wide list lacks gains it nothing. The gateway-wide check runs first, so a target the gateway excludes is reported as a gateway setting to change, not as a key setting.
Absent and empty mean different things:
| Value | Meaning |
|---|---|
field absent or null |
inherit the gateway-wide list; this key adds no narrowing |
| non-empty list | narrow to these, within what the gateway-wide list already allows |
[] |
deny every target of that kind for this key |
["*"] |
inherit the gateway-wide list (a key's * cannot widen it) |
On the settings page each field has a restrict checkbox: unticked saves null (inherit), ticked with nothing named saves [] (deny all).
Notes:
dbConnectionsnarrowsdb_queryonly, including adb_queryreached throughfederation_call. See Database access.- The scope is resolved once per request, so a settings save landing mid-request cannot split one decision across two policies.
- The same per-key scope applies to calls arriving from another gateway over the Gateway Network, which authenticate against this gateway's own key list.
- An OAuth caller is not in
auth.keys, so it is not narrowed per key; the gateway-wide gates still apply in full. - In per-key allowlists, a script endpoint is always written
scripting/<project>/<name>.
Per-key read scoping¶
The settings above narrow what a key may change. Read lists narrow what it may read, by path. A key can carry tagReadAllowlist, resourceReadAllowlist and configReadAllowlist:
{
"auth": {
"keys": [
{ "name": "pump-watcher", "token": "...", "profile": "diag",
"tagReadAllowlist": ["[default]Pumps/"],
"resourceReadAllowlist": [],
"configReadAllowlist": [] },
{ "name": "view-auditor", "token": "...",
"resourceReadAllowlist": ["MesProject/com.inductiveautomation.perspective/views/"] },
{ "name": "admin", "token": "..." }
]
}
}
Here pump-watcher may read tags under [default]Pumps and no project or config resource at all, whatever its profile allows. view-auditor may read one project's views and is unrestricted on tags and config. admin reads whatever its profile exposes, over every path.
There is no gateway-wide read list: without a per-key list, a key reads every path its profile's tools can reach. A read list narrows against that whole surface, and every refusal names the key's own list, which is always the one to edit.
| List | Entries are | Examples |
|---|---|---|
tagReadAllowlist |
canonical tag paths, spelled as tag_write spells its targets |
[default]Pumps/, [default] (a whole provider) |
resourceReadAllowlist |
<project>/<moduleId>/<typeId>/<path>, the same as the project-resource mutation target |
MesProject/, MesProject/com.inductiveautomation.perspective/views/Line1 |
configReadAllowlist |
config/<moduleId>/<typeId>/<path>, the same as the config-resource mutation target |
config/ignition/database-connection/MesDB |
Absent, non-empty, [] and ["*"] mean what they mean for the mutation lists, except that "inherit" means "the whole surface your profile exposes".
Matching rules.
- Entries match with the same boundary rules as the mutation allowlist:
[default]Pumps/covers[default]Pumps/P1and never[default]PumpsX. A tag entry ending in]covers that whole provider. - There are no
provider:toolentries here. Which tools a key may call is the job of its tool profile. - Only canonical paths are matched. For a key carrying a list of that kind, a path containing
.,.., an empty interior segment or a backslash (Public/../Secret/X), or a tag path whose folder part opens a second provider ([default][other]Plant), is refused whatever the list says.
A tag definition is a tag through either route. On 8.3, tag and UDT definitions are also config resources (ignition/tag-definition, ignition/tag-type-definition). config_resource_read on one is matched against tagReadAllowlist too ([provider]<folder>, or [provider]_types_/<folder>), and config_resources and config_manifest leave out the tag and UDT definition entries the key's tag list does not cover.
A child project reads its parents. ref_search, ref_usages, project_resource_read, view_bindings and the Vision read tools resolve through the project's inheritance chain. A key scoped to a child project keeps reading what the child defines, and needs the parent's paths in its list for what the child inherits. project_manifest and deploy_diff also need the parents listed.
Historian paths are matched in the tag list. history_browse and tag_history paths are spelled differently from tag paths, but they return the same values a tag read does, so they are gated by tagReadAllowlist. A key narrowed to [default]Pumps/ is refused history reads until its list also names the historian path.
Whole-surface reads are refused, not narrowed. project_export, the manifests, an unscoped ref_search and export_diagnostics_bundle read everything they can reach rather than a named path. For a key carrying that kind of list they are refused unless the list is ["*"]. A refusal is always reported as a refusal, never as an empty result, so a caller is never told "no such tag" when it was in fact refused.
Write tools that echo existing content are gated as reads too. view_patch returns the node at each pointer as before (and dryRun returns it without writing); history_provider_edit and eam_task_update return the existing config resource. All three are checked against the read lists as well as the mutation allowlist.
What read scoping does not cover
Only tools that address tags, project resources or config resources are gated by the read lists. Gateway status, alarms, the audit log, Perspective sessions, users and roles, the EAM status tools, historian inventory and db_query are not; db_query has its own per-key list, and the rest are governed by the tool profile alone. MCP resources and prompts are also governed by the profile alone.
To see exactly which tools are gated for your key, read caller.readScopeCoverage from mcp_status, or the readScope.key row that policy_explain reports for a tool.
Calls to other gateways. federation_call and federation_call_mutating apply the calling key's tool profile to the remote tool's name, and its read and database lists to the remote tool's arguments. A remote tool this gateway does not also have is refused to any key carrying a list. Both tools refuse this gateway and its redundancy partner as targets, and a receiving gateway refuses a call the Gateway Network does not attribute to a source gateway.
Redundant pairs¶
On a redundant pair the module runs on both nodes and serves reads on both. Writes depend on the node's place in the pair, because a backup cannot keep a configuration change: the master's sync overwrites it.
| Kind of write | Standalone or master | Backup, Cold or Warm | Backup, Active (failover) |
|---|---|---|---|
Persistent: project and config resources, view_patch, deploy_apply, users and roles, rosters, historian providers, EAM tasks, a mutating api_call |
allowed | refused | refused |
Runtime: tag_write, alarm_acknowledge, alarm_shelve, alarm_unshelve, close_session, send_session_message, script endpoints |
allowed while Active | refused | allowed |
Node-local: logger_level_set, this node's store-and-forward quarantine, federation_call_mutating (the write lands on another gateway) |
allowed | allowed | allowed |
- Any mutating tool not named above is Persistent, so it is refused on a backup by default.
- Runtime writes follow activity, not role. A standby runs no tags and serves no sessions; a backup that has taken over is the node operating the plant, so it accepts them. A master that is not Active (for example in manual recovery mode before it is recovered) refuses them the same way. A Runtime write accepted on an Active backup, such as a memory tag value, is still in place on the master after it recovers.
- The refusal names where to go instead, taken from the backup's own redundancy settings, and carries a
remediationlike every other refusal. AdryRun: truecall is refused exactly as the real call is, so it can never report a clean plan for a write the backup would discard. policy_explainreports it as theredundancyrow, afterdbConnections.gatewayand before the mutation allowlists: the fix for this refusal is a different gateway, not a different allowlist entry. For a read, the row readsnotApplicable.mcp_statusand/data/artek-mcp/statusinclude anodeblock (role,activityLevel). It is on the unauthenticated/statusroute so a load balancer can find the Active node without a key.- Both nodes share one settings resource. The master's settings sync to the backup and reload there without a restart, so the API keys and the read-only and allowlist policy are the same on both nodes.
- There is no override. A standalone gateway (role Independent) is never refused, and a redundancy state that cannot be read is treated as standalone.
Settings reference¶
Settings named in this manual, by area:
| Area | Keys |
|---|---|
| Policy | readOnly, localhostOnly, mutation.allowlist, mutation.policyVersion, mutation.requireNameEcho |
| Rate | http.requestsPerMinute, http.writesPerMinute |
| Authentication | auth.mode, auth.keys[], auth.oauth.issuer, auth.oauth.jwksUrl, auth.oauth.audience, auth.oauth.requiredScopes, auth.oauth.cacheSeconds |
Per key (auth.keys[]) |
name, token, profile, mutationAllowlist, dbConnections, tagReadAllowlist, resourceReadAllowlist, configReadAllowlist, requestsPerMinute, writesPerMinute |
| Profiles | profiles.<name>.description, profiles.<name>.tools |
| Providers | providers.<id> (true or false) |
| Database | db.connections, db.maxRows, db.maxCellChars, db.maxResponseChars |
| Script endpoints | scripting.endpoints[] |
| REST bridge | api.gatewayApiKey, api.baseUrl |
Origin validation, the allowed origins, the body cap and the tool timeout are edited on the settings page.