Building a custom MCP server in Ruby so LLMs can control an Elgato Key Light

Edu Depetris

- Mar 28, 2026
  • Ruby
  • Ai
  • Mcp
  • Docker
  • Llms
  • Ai Tools
TL;DR

I built a small MCP server in Ruby to control my Elgato Key Light from an LLM.

Totally unnecessary.
Also very fun.

Let’s Overengineering how to switch a light on and off


I have a beautiful Elgato Key Light that I’d love to control from any chatbot, like ChatGPT, Claude, or any other LLM-powered tool.

Even though the light can be controlled with a very simple HTTP call (if you know the IP address), I wanted to overengineering it a bit and build my own MCP server so LLMs can control it.

The idea

We’ll build a small MCP server in Ruby that exposes tools to:

- Turn the light on/off  
- Adjust brightness and temperature  
- Read the current state  

Stack

- Ruby 4  
- Bundler  

A bit about MCP

The Model Context Protocol (MCP) supports two main transports:

- **stdio** for local, process-based communication  
- **HTTP / SSE**  for remote communication  

These transports use JSON-RPC to handle messages.

For this example, we’ll keep things simple and use **stdio**.


Step 1: Talk to the light


First, let’s implement two simple HTTP methods to control and read the light.

module ElgatoKeyLight
  def self.set_light_state(on: ON, brightness: 75, temperature: MIN_TEMPERATURE)
    uri = URI('http://192.168.100.13:9123/elgato/lights')
    http = Net::HTTP.new(uri.host, uri.port)
    request = Net::HTTP::Put.new(uri.path, 'Content-Type': 'application/json')

    body = { "lights": [ {"on":on,"brightness":brightness,"temperature":temperature} ] }.to_json
    request.body = body

    response = http.request(request)
    response.code
  end

  def self.get_light_state
    uri = URI('http://192.168.100.13:9123/elgato/lights')
    http = Net::HTTP.new(uri.host, uri.port)
    request = Net::HTTP::Get.new(uri.path, 'Content-Type': 'application/json')

    response = http.request(request)
    JSON.parse(response.body)
  end
end

Now we can talk to the light programmatically.


Step 2: Expose MCP tools


class ElgatoKeyLightControlTool < MCP::Tool
  class << self
    def call(on:, brightness:, temperature:)
      result = ElgatoKeyLight.set_light_state(on: on, brightness: brightness, temperature: temperature)

      MCP::Tool::Response.new([{
        type: "text",
        text: "Light state set with response code: #{result}",
      }])
    rescue => e
      MCP::Tool::Response.new([{ type: "text", text: JSON.generate({ error: e.message }) }])
    end
  end
end

class ElgatoKeyLightStatusTool < MCP::Tool
  class << self
    def call
      result = ElgatoKeyLight.get_light_state

      MCP::Tool::Response.new([{
        type: "text",
        text: JSON.generate(result),
      }])
    rescue => e
      MCP::Tool::Response.new([{ type: "text", text: JSON.generate({ error: e.message }) }])
    end
  end
end


Step 3: Create the MCP server


Let's create a simple server and use STDIO to listen and talk:

server = MCP::Server.new(
  name: "elgato_key_light_control_server",
  tools: [ElgatoKeyLightControlTool, ElgatoKeyLightStatusTool],
)

transport = MCP::Server::Transports::StdioTransport.new(server)
transport.open


Step 4: Run and test


Finally let's run our server and test it out with elgato_key_light_status_tool

Start the server:

$ ruby stdio_server.rb

Send a request:

{"jsonrpc": "2.0","id": 1,"method": "tools/call","params": {"name": "elgato_key_light_status_tool","arguments": {}}}

Response:

{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"{\"numberOfLights\":1,\"lights\":[{\"on\":0,\"brightness\":0,\"temperature\":143}]}"}],"isError":false}}

It's alive!


Alternative you can use

You can also use the MCP inspector:

npx @modelcontextprotocol/inspector ruby stdio_server.rb
npx-mcp-debuggin.png 55.5 KB

Plug it into an LLM

Now you can whitelist your MCP server in Claude (or any compatible client) and control your light directly from chat.
claude-mcp-elgato.mp4 234 KB


Notes

I also created a Dockerfile to test this easily.

However, it won’t work out of the box on macOS due to Docker network limitations when accessing local network devices.
There are workarounds—but I didn’t include them in this repo.


Happy clauding!