oci ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4stdioMITupdated 1mo ago
An MCP server that lets an LLM read, write, browse, search, and subscribe to live data on an OPC-UA industrial automation server - over stdio or HTTP, with a persistent cache and a searchable index of the address space built in.
What can you do with OPC UA MCP Server?
OPC-UA MCP Server
An MCP server that lets an LLM read, write, browse, search, and subscribe to live data on an OPC-UA industrial automation server - over stdio or HTTP, with a persistent cache and a searchable index of the address space built in.
https://github.com/user-attachments/assets/0b676e6e-17ce-42f5-918f-9a615e939008
Quick start
The fastest way to see it working end-to-end, using the bundled Microsoft OPC-UA test server and a public tunnel Claude can reach:
git clone https://github.com/mwieczorkiewicz/opcua-mcp.git
cd opcua-mcp
make compose-up # starts a test OPC-UA server, opcua-mcp, and a public HTTPS tunnel
make connector-url # prints a URL like https://xyz.trycloudflare.com/mcp
Paste that URL into Claude at Settings → Connectors → Add custom
connector, then ask it to browse the server or read a value. Stop with
make compose-down when you're done - see
docs/deployment.md for what that tunnel exposes and
how to run against your own OPC-UA server instead.
Building and running locally
go build -o opcua-mcp ./cmd/opcua-mcp.go
# stdio (default) - no OPC-UA connection until the client calls opcua_connect
./opcua-mcp
# HTTP - connects eagerly at startup
SERVER_TRANSPORT=http OPCUA_ENDPOINT=opc.tcp://localhost:4840 ./opcua-mcp
Requires Go 1.26+ and, optionally, Docker for the test server / containerized deployment.
What it does
- Read / write node values, with type validation on writes so a mismatched value is rejected before it reaches the device.
- Browse the address space one level at a time or recursively, and look nodes up by name instead of by node ID.
- Subscribe to push-based live updates - subscriptions persist across restarts and are automatically re-established on reconnect.
- Cache reads, browse results, and type info on disk (bbolt), so repeat lookups don't round-trip to the device; writes invalidate the relevant entry automatically.
- Discover and search the address space in the background, indexed with Bleve for fuzzy/partial browse-name lookups.
- Anonymous, username/password, or certificate auth, with configurable OPC-UA security policy and mode.
See docs/architecture.md for how the caching layer, subscription manager, and discovery index fit together.
MCP tools
| Tool | Description |
|---|---|
opcua_read |
Read one or more node values. Subscribed nodes are served from the live cache; others go live unless max_age_ms allows a cached value. |
opcua_write |
Write a value to a node. Validates the value's type against the node before writing. |
opcua_get_value |
Read a single node's value - a convenience wrapper over opcua_read. |
opcua_get_value_by_name |
Read a value by browse name instead of node ID, via the discovery index. |
opcua_browse |
List a node's immediate children. |
opcua_browse_nodes |
Recursively browse from a node up to a depth limit, nesting children under their parent. |
opcua_node_info |
Get a node's metadata (data type, access level, etc.). |
opcua_find_similar_nodes |
Fuzzy-match browse names against the discovery index. |
opcua_subscribe |
Start push-based updates for one or more nodes at a given interval. |
opcua_unsubscribe |
Cancel a subscription, by ID or by naming one of its nodes. |
opcua_list_subscriptions |
List active subscriptions. |
opcua_connect / opcua_disconnect |
Manage the connection explicitly (mainly relevant in stdio mode). |
opcua_server_info |
Get OPC-UA server metadata. |
opcua_discovery_stats |
Stats on the background discovery cache (node count, depth distribution, enabled flags). |
opcua_force_discovery |
Trigger an immediate discovery refresh instead of waiting for the next cycle. |
opcua_debug_search / opcua_ensure_server_nodes |
Diagnostics for troubleshooting why a node isn't showing up in search. |
MCP resources
| Resource | Description |
|---|---|
opcua://node/{node_id} |
Node data, e.g. opcua://node/ns=2;i=1. Accepts a comma-separated list for multiple nodes. |
opcua://server |
OPC-UA server information. |
Configuration
Configuration is loaded (via viper) from three sources, in ascending order of precedence:
- Built-in defaults (shown in the tables below).
- An optional config file - TOML, YAML, JSON, or any other format viper
supports. By default
./config.{yaml,yml,toml,json,...}is read if present; point at an explicit path withCONFIG_FILE=/path/to/config.toml. A config file is entirely optional - env vars alone are still enough. - Environment variables (
SERVER_*,OPCUA_*,MCP_*,SEARCH_*,STORE_*) - always win over the config file, so existing env-var-only deployments keep working unchanged.
A config file mirrors the env var names, lowercased and nested under each
prefix, e.g. SERVER_HTTP_PORT becomes:
server:
http_port: "8080"
Server
| Variable | Default | Description |
|---|---|---|
SERVER_TRANSPORT |
stdio |
stdio or http |
SERVER_HTTP_PORT |
8080 |
Port for HTTP transport |
SERVER_LOG_LEVEL |
info |
debug, info, warn, error |
SERVER_LOG_FORMAT |
json |
json or text |
SERVER_LOG_OUTPUT |
stdout |
stdout, stderr, or file (forced to stderr in stdio mode, since stdout carries the MCP stream) |
SERVER_LOG_FILE |
- | Log file path, required if SERVER_LOG_OUTPUT=file |
SERVER_LOG_ADD_SOURCE |
false |
Add source file/line to log entries |
OPC-UA connection
| Variable | Default | Description |
|---|---|---|
OPCUA_ENDPOINT |
opc.tcp://localhost:4840 |
Server endpoint |
OPCUA_AUTH_MODE |
anonymous |
anonymous, username, or certificate |
OPCUA_USERNAME / OPCUA_PASSWORD |
- | Required if AUTH_MODE=username |
OPCUA_CERT_FILE / OPCUA_KEY_FILE |
- | Required if AUTH_MODE=certificate |
OPCUA_SERVER_CERT |
- | Server certificate file path |
OPCUA_SECURITY_POLICY |
None |
None, Basic128Rsa15, Basic256, Basic256Sha256, Aes128_Sha256_RsaOaep |
OPCUA_SECURITY_MODE |
None |
None, Sign, SignAndEncrypt |
OPCUA_REQUEST_TIMEOUT |
30s |
Per-request timeout |
OPCUA_SESSION_TIMEOUT |
60s |
Session timeout |
OPCUA_MAX_RETRIES |
3 |
Connection retry attempts |
OPCUA_RETRY_DELAY |
1s |
Delay between retries |
MCP
| Variable | Default | Description |
|---|---|---|
MCP_NAME |
OPC-UA MCP Server |
Server name reported to clients |
MCP_VERSION |
1.0.0 |
Server version reported to clients |
MCP_ENABLE_TOOLS |
true |
Enable tools |
MCP_ENABLE_RESOURCES |
true |
Enable resources |
MCP_ENABLE_PROMPTS |
false |
Enable prompts |
MCP_HTTP_PATH |
/mcp |
HTTP endpoint path |
Discovery and search
| Variable | Default | Description |
|---|---|---|
SEARCH_ENABLE_DISCOVERY |
true |
Enable background node discovery |
SEARCH_DISCOVERY_INTERVAL |
30s |
How often to re-crawl the address space |
SEARCH_DISCOVERY_ROOT_NODE |
i=85 |
Root node to crawl from (Objects folder) |
SEARCH_MAX_DISCOVERY_DEPTH |
10 |
Maximum crawl depth |
SEARCH_MAX_NODES_PER_BROWSE |
10000 |
Cap on nodes returned per browse call |
SEARCH_ENABLE_SEARCH |
true |
Enable the Bleve search index |
SEARCH_INDEX_PATH |
./search_index |
Search index directory |
SEARCH_MAX_RESULTS |
100 |
Max results per search |
SEARCH_MIN_SCORE |
0.1 |
Minimum match score |
SEARCH_ENABLE_CACHE |
true |
Master switch for read-through caching. false makes every opcua_read/opcua_write/opcua_browse_nodes call go live, matching pre-cache behavior exactly |
Persistent store
Backs read-through caching and subscription persistence with an on-disk bbolt database.
| Variable | Default | Description |
|---|---|---|
STORE_DB_PATH |
mcp_opcua_store.db |
Database file path |
STORE_OPEN_TIMEOUT |
5s |
How long to wait for the file lock on open |
STORE_TYPEINFO_TTL |
24h |
Freshness window for cached type info |
STORE_BROWSE_TTL |
5m |
Freshness window for cached browse results |
STORE_BATCH_WINDOW |
25ms |
How often subscription notifications flush to the store |
STORE_BATCH_MAX_ITEMS |
250 |
Max notifications flushed per batch |
STORE_NOTIFY_CHAN_BUFFER |
1024 |
Buffer size for incoming subscription notifications |
If the store fails to open (e.g. a stale lock from a prior ungraceful shutdown, or a read-only filesystem), the server logs a warning and keeps running with caching forced off and subscription tools returning an error - every other tool is unaffected.
Telemetry
opcua-mcp collects anonymous, aggregate usage telemetry (which tools get used, cache hit rate, error categories - never node IDs, endpoint URLs, node values, or credentials) to help prioritize maintenance of this open-source project. It's on by default; see docs/telemetry.md for exactly what is and isn't collected.
Opt out with either:
| Variable | Effect |
|---|---|
DO_NOT_TRACK=1 |
The cross-project community convention (consoledonottrack.com) |
OPCUA_MCP_TELEMETRY=false |
This project's own switch |
Docker
docker build -t opcua-mcp .
docker run -p 8080:8080 -e SERVER_TRANSPORT=http -e OPCUA_ENDPOINT=opc.tcp://your-server:4840 opcua-mcp
Multi-stage build on Chainguard's minimal Go image, running from scratch -
no shell, small attack surface. Mount ./search_index and
./mcp_opcua_store.db as volumes to persist discovery/cache/subscription
state across restarts. Full auth-mode examples, the Compose dev stack, and
the Claude-connector tunnel setup are in
docs/deployment.md.
Development
make start-opcua-server # Microsoft OPC-UA test server in Docker
make run-with-test-server # run the app against it (auto start/stop)
go test ./... # unit tests
go test -race ./...
make test-integration # real Subscribe/reconnect/cache behavior via testcontainers-go (needs Docker)
VS Code launch configs are in .vscode/launch.example.json - copy to
.vscode/launch.json to get stdio/HTTP/auth debug targets that start and
stop the test server automatically. make help lists every available
target.
Tests are table-driven and mock the OPC-UA client at the opcuaClient
interface seam (internal/opcua/mock_client_test.go) rather than against a
live/simulated server - see docs/architecture.md for
how the pieces being tested fit together, and
docs/COMMIT_CONVENTION.md for this repo's
commit message format.
Contributing
Fork it, make your changes, open a PR - see CONTRIBUTING.md.
License
Install
Add OPC UA MCP Server to your client. Pick the one you use.
claude mcp add ghcr-io-mwieczorkiewicz-opcua-mcp-main-2 -- docker run -i --rm ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4codex mcp add ghcr-io-mwieczorkiewicz-opcua-mcp-main-2 -- docker run -i --rm ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4amp mcp add ghcr-io-mwieczorkiewicz-opcua-mcp-main-2 -- docker run -i --rm ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4{
"mcpServers": {
"ghcr-io-mwieczorkiewicz-opcua-mcp-main-2": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4"
]
}
}
}Add to `claude_desktop_config.json`, then restart Claude Desktop.
{
"mcpServers": {
"ghcr-io-mwieczorkiewicz-opcua-mcp-main-2": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4"
]
}
}
}Add to `~/.cursor/mcp.json`, or `.cursor/mcp.json` for a single project.
code --add-mcp '{"name":"ghcr-io-mwieczorkiewicz-opcua-mcp-main-2","command":"docker","args":["run","-i","--rm","ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4"]}'Or add the block manually to `.vscode/mcp.json` under `servers`.
{
"mcpServers": {
"ghcr-io-mwieczorkiewicz-opcua-mcp-main-2": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4"
]
}
}
}Add to `~/.codeium/windsurf/mcp_config.json`.
{
"mcpServers": {
"ghcr-io-mwieczorkiewicz-opcua-mcp-main-2": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4"
]
}
}
}Add to `cline_mcp_settings.json` via the MCP Servers panel.
{
"mcpServers": {
"ghcr-io-mwieczorkiewicz-opcua-mcp-main-2": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4"
]
}
}
}Add to `~/.gemini/settings.json`.
{
"mcpServers": {
"ghcr-io-mwieczorkiewicz-opcua-mcp-main-2": {
"type": "local",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4"
],
"tools": [
"*"
]
}
}
}Add to `~/.copilot/mcp-config.json`, or run `/mcp add` inside the CLI.
{
"context_servers": {
"ghcr-io-mwieczorkiewicz-opcua-mcp-main-2": {
"command": {
"path": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4"
]
}
}
}
}Add to your Zed `settings.json`.
docker run -i --rm ghcr.io/mwieczorkiewicz/opcua-mcp:main-29734c4Run `goose configure`, choose **Add Extension → Command-line Extension**, and paste this command.
Score
39 / 100
Incomplete
- Documentation25/25
- Maintenance19/25
- Trust13/20
- Capability0/15
- Install experience12/15
- Documents what it does and how to connect
- Has a resolvable package or endpoint
- Exposes at least one tool, prompt or resource
- README has substantive content
- Includes a code example
- Documents its configuration
- Mentions credentials or security posture
- Last commit 26 days ago
- Has a release history
- Repository is not archived
- Licensed MIT
- Namespace verified in the official MCP registry
- Claimed by its owner
- Published under an organisation
- 0 tool(s) documented
- Provides prompt templates
- Provides resources
- 12 documented install method(s)
- Published to a package registry
- Offers a hosted endpoint — no local install
Version history
| Versions | Published |
|---|---|
| 1.0.0Latest | Aug 5, 2026 |