dirigera-mcp

dirigera-mcp

An MCP server that lets an MCP client control IKEA TRÅDFRI smart plugs paired to a DIRIGERA hub via its local REST API.

Category
访问服务器

README

dirigera-mcp

An MCP server that lets an MCP client (Claude Code) control IKEA TRÅDFRI smart plugs ("strömbrytare") paired to a DIRIGERA hub.

It talks to the hub over its local REST API (HTTPS on port 8443, bearer-token auth) using the dirigera library. Nothing goes through IKEA's cloud.

Transports stdio, or streamable-http for sharing one hub connection across devcontainers
SDK official Python MCP SDK (mcp 2.0.0, MCPServer)
Hub client dirigera 1.2.7
Server src/dirigera_mcp/server.py
Pairing helper scripts/get_token.py

Setup

The hub issues a long-lived access token only after someone physically presses its action button. That token then has to reach the container, and it does so from your host environment via devcontainer remoteEnv — it is never committed, never written to a file in this repo, and never logged.

Do these three steps in order.

1. Generate the token

In a terminal inside the container:

python scripts/get_token.py

The script prints the hub address, then tells you to press the action button (the small recessed button on the underside of the hub). Press it, come back, hit ENTER, and the token is printed to stdout. You have about 60 seconds.

If you are driving this non-interactively, use the countdown mode instead — no ENTER needed:

python scripts/get_token.py --wait 30

The address comes from $DIRIGERA_HUB_IP; pass --ip <address> if it is not set yet.

The token is printed once and is not saved anywhere. Copy it now. If you lose it, just run the script again — pairing again is harmless and does not invalidate other tokens.

Prefer running this in your own VS Code terminal rather than having Claude run it, so the credential does not end up in a chat transcript.

2. Put it in your HOST user environment

Not in the container, and not in a .env file — the whole point of the setup is that the credential lives on the host and is injected at runtime.

On Windows, in a PowerShell window on the host:

[Environment]::SetEnvironmentVariable("DIRIGERA_TOKEN", "<paste-the-token>", "User")
[Environment]::SetEnvironmentVariable("DIRIGERA_HUB_IP", "<your-hub-ip>", "User")

On macOS/Linux hosts, put the two export lines in your shell profile (~/.zshrc, ~/.bashrc) and make sure VS Code is launched from a shell that has sourced it.

DIRIGERA_HUB_IP is required. It has no default in the source: a baked-in fallback would be one specific person's LAN address. It is also what the sandbox firewall opens its single exception for, so the same value drives both.

3. Restart VS Code and reopen the container

First confirm the values really persisted. In PowerShell — this reads the stored User-scope value directly, so it works even in the window that just set it:

[Environment]::GetEnvironmentVariable("DIRIGERA_TOKEN", "User").Length   # expect ~400, not 0
[Environment]::GetEnvironmentVariable("DIRIGERA_HUB_IP", "User")         # expect your hub address

Then restart VS Code. A Windows process inherits its environment at launch and never sees User variables created afterwards, so a window reload — and even a container rebuild — is not enough: the value comes from the running VS Code process, not from the container.

  1. Quit every VS Code instance, including windows holding unrelated projects. VS Code runs one shared main process on Windows and new windows inherit its environment, so a single surviving window is enough to keep the stale environment alive. code . from a fresh shell does not help either — it just signals the existing instance.
  2. Confirm nothing survived: Get-Process code -ErrorAction SilentlyContinue should print nothing. If processes linger without visible windows, ... | Stop-Process (save your work first).
  3. Start VS Code again, open this folder, and Reopen in Container.

No rebuild is needed. Closing the last window stops the container (shutdownAction defaults to stopContainer), and the next attach re-reads remoteEnv.

Verify inside the container, in a bash terminal (this is bash syntax; it silently prints empty values if you run it in PowerShell):

echo "hub=$DIRIGERA_HUB_IP token_length=${#DIRIGERA_TOKEN}"

A non-zero token_length means the token arrived. Then restart Claude Code so it picks up the dirigera server from .mcp.json, and ask it to list your outlets.

Troubleshooting

The variables exist in the container but are empty (env | grep DIRIGERA shows both names with no values). remoteEnv is working; ${localEnv:...} resolved to nothing. That means VS Code was started before the host variables were created — quit it fully and relaunch, as above.

Still empty after a full restart. Check whether VS Code is reading the Windows environment at all: if /home/vscode/.gitconfig-host exists and is non-empty, ${localEnv:USERPROFILE} resolved, so the mechanism works and only the DIRIGERA values are missing. If that file is missing, VS Code is resolving localEnv somewhere else — typically because the window was opened through Remote-WSL, where Windows User variables are not visible unless forwarded via WSLENV. Set the two variables inside WSL instead, or open the folder as a Windows path.

The tools work but the plug does not switch. Check is_reachable in the result. The hub accepts writes for offline devices, so the server flags this with a warning field rather than reporting a state it cannot confirm.


MCP tools

Tool Arguments Returns
list_outlets — every outlet: id, name, room, is_on, is_reachable
get_outlet outlet_id current state of one outlet, read fresh from the hub
set_outlet outlet_id, on (bool) new state, plus previous_is_on
toggle_outlet outlet_id new state, plus previous_is_on
power_cycle outlet_id, off_seconds (0.5–300, default 5) state after power is restored

power_cycle switches the outlet off, waits, and switches it back on — for rebooting hardware such as a development board. Power is restored on every path out of the wait, and the call refuses outright on an unreachable outlet rather than risk cutting power it cannot restore. The call blocks for off_seconds, so keep it well inside the client's tool-call timeout.

set_outlet and toggle_outlet re-read the outlet from the hub after writing, so the state they report is the hub's, not an optimistic guess. If the hub reports the plug as unreachable, the result carries a warning field — the hub accepts writes for offline devices, so a successful call is not by itself proof that the plug switched.

Every failure comes back as a plain sentence, not a stack trace:

Situation What the client sees
DIRIGERA_TOKEN empty "DIRIGERA_TOKEN is not set in this container's environment…" + how to fix
Token wrong/expired (401/403) "The hub rejected the token…" + how to regenerate
Hub off or wrong IP "Cannot reach the DIRIGERA hub at <ip>:8443…"
Unknown outlet id "Device id not found. Call list_outlets to get the ids…"
Id belongs to a lamp, not a plug "Device is not an outlet. Call list_outlets…"

Registration

The server is registered in the repo's .mcp.json alongside the other project servers:

"dirigera": {
    "type": "stdio",
    "command": "/workspaces/ikea-mcp/.venv/bin/python",
    "args": ["/workspaces/ikea-mcp/src/dirigera_mcp/server.py"],
    "env": {
        "DIRIGERA_HUB_IP": "${DIRIGERA_HUB_IP:-}",
        "DIRIGERA_TOKEN": "${DIRIGERA_TOKEN:-}"
    }
}

Absolute paths, because the client chooses the working directory. The :- defaults let the server start even with the variables unset, so an unconfigured setup produces a helpful tool error instead of a server that fails to launch.

The server is a single module run directly by the venv interpreter — the project is not installed as a package, so there is no build step and uv sync is all that is needed.


Sharing one server with other devcontainers

An MCP stdio server is spawned as a child process by its client, so Claude Code running inside devcontainer X always starts the server inside X. Installing it on the host does nothing for a containerised client. To use one hub connection from several projects, run the server over HTTP.

The payoff: the hub token and the firewall's LAN exception exist in exactly one place. Project containers get neither — they only reach host.docker.internal.

Run it on the host

docker compose from deploy/, with both secrets in the shell's environment:

$env:DIRIGERA_MCP_KEY = python -c "import secrets; print(secrets.token_urlsafe(32))"
[Environment]::SetEnvironmentVariable("DIRIGERA_MCP_KEY", $env:DIRIGERA_MCP_KEY, "User")
cd deploy
docker compose up -d --build

DIRIGERA_MCP_KEY is the shared secret between the server and every client. Without it the server refuses to start over HTTP — an unauthenticated endpoint here can cut power to whatever is plugged in.

To run the published image instead of building locally — no clone needed on the host:

docker run -d --name dirigera-mcp --restart unless-stopped `
  -p 127.0.0.1:8765:8765 `
  -e DIRIGERA_HUB_IP -e DIRIGERA_TOKEN -e DIRIGERA_MCP_KEY `
  ghcr.io/david-s-svedberg/ikea-mcp:latest

.github/workflows/publish-image.yml builds and publishes that image on every push to main, using the workflow's built-in GITHUB_TOKEN. The image holds no credentials; both secrets arrive as runtime environment.

Point a project at it

In that project's .mcp.json:

"dirigera": {
    "type": "http",
    "url": "http://host.docker.internal:8765/mcp",
    "headers": { "Authorization": "Bearer ${DIRIGERA_MCP_KEY}" }
}

The project's devcontainer needs DIRIGERA_MCP_KEY in remoteEnv (same host-variable flow as the token above) and must allow egress to host.docker.internal. Under the firewall pattern in .devcontainer/init-firewall.sh that is already covered by the 192.168.65.0/24 carve-out for Docker Desktop's internal services. No LAN exception is needed.

Security properties

Check Behaviour
Missing or wrong Authorization 401, constant-time comparison so the secret does not leak byte by byte
Forged Host header 421, DNS-rebinding protection with an explicit allow-list
Published port host loopback only, so the endpoint is not on the LAN
Image contents no credentials; both secrets arrive as runtime environment

Verified locally over HTTP: 401 without and with a wrong secret, 421 on a forged Host header, and a working session against the real hub with the correct one.

Loopback publishing is enough — verified on Docker Desktop for Windows. With the port published as 127.0.0.1:8765:8765, a devcontainer resolves host.docker.internal to the Docker Desktop gateway (192.168.65.254), which proxies through to the host's loopback and reaches the server. The port stays off the LAN while remaining reachable from containers.

If a different Docker setup cannot do that, publish on all interfaces ("8765:8765") and block the port from outside in the host firewall, or put the server and the project container on a shared user-defined Docker network and address it by container name.

Sandbox notes

This devcontainer is hardened (see .devcontainer/init-firewall.sh): egress to RFC1918 space is rejected, with exactly one exception for $DIRIGERA_HUB_IP:8443.

No LAN address is committed anywhere in this repo. The firewall script takes the hub address as an argument, which devcontainer.json fills from remoteEnv, and the Python side reads the same variable. Renumbering the hub therefore means changing one value in your host environment. If DIRIGERA_HUB_IP is unset the script falls back to RFC 5737 documentation space, so it fails closed: the exception points at an address nothing answers on, and the real hub stays blocked.

Interactive sudo is disabled. System packages belong in .devcontainer/Dockerfile, followed by a container rebuild.

The hub serves a self-signed certificate, so the dirigera library disables TLS verification. That is the library's own behaviour and acceptable here: the connection is to a single pinned LAN IP that the firewall restricts to one port.

Development

uv sync                       # install locked deps into .venv
ruff check src scripts        # lint

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选