tinymcp: Unlocking the Physical World for LLMs with MCP and Microcontrollers

Today we are launching tinymcp, a Model Context Protocol (MCP) server and framework that enables any connected device to expose remote functionality to Large Language Models (LLMs). While many MCP servers expand the capabilities of LLMs, few are able to enable direct interaction in the physical world. tinymcp leverages Golioth’s optimized cloud services and firmware SDK to allow LLMs to interact with even the most constrained devices.

Asking Gemini to turn of the LED via the tinymcp server.

tinymcp is an experimental project and will be evolving rapidly with breaking changes. Extreme caution should be taken when delegating physical capabilities to AI systems.

Background

It seems like every company is racing to implement MCP servers that allow LLMs to interact with their API to satisfy the prompts of users. As suggested by the name, the seeming primary use case of MCP is providing additional context to LLMs, allowing them to return more useful responses by accessing relevant data from external sources. For example, a Golioth MCP server could enable users to leverage LLMs to glean high-level insights about their device fleets by exposing data via Golioth’s Management API.

This is a compelling use case, and one we will likely support in the coming months, but it is far from novel at this point. Instead, tinymcp is an effort to enable users to easily run their own MCP servers on embedded devices, and expose the functionality remotely. This is built on Golioth’s existing LightDB State and Remote Procedure Call (RPC) support. It leverages MCP’s tool calling capabilities, making it possible to expose arbitrary functionality on a device to an LLM. The tinymcp MCP server acts a proxy, translating MCP clients’ JSON-RPC API calls to Golioth RPCs, which are then delivered to devices.

How It Works

Because tinymcp leverages existing Golioth features, current and previous versions of the Golioth Firmware SDK can be used to implement an on-device MCP server. In fact, existing devices running firmware that register RPCs can expose the functionality via MCP without any firmware changes. Additionally all platforms and hardware supported by the SDK can be targeted. In the following blinky example, a simple Zephyr firmware application exposes the ability to turn an LED on and off via MCP tool calls.

/*
 * Copyright (c) 2025 Golioth, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(blinky, LOG_LEVEL_DBG);

#include <golioth/client.h>
#include <golioth/rpc.h>
#include <samples/common/sample_credentials.h>
#include <samples/common/net_connect.h>
#include <string.h>
#include <zephyr/kernel.h>

#include <zephyr/drivers/gpio.h>

#define LED0_NODE DT_ALIAS(led0)

static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(LED0_NODE, gpios);

static K_SEM_DEFINE(connected, 0, 1);

static enum golioth_rpc_status on_light_on(zcbor_state_t *request_params_array,
                                           zcbor_state_t *response_detail_map,
                                           void *callback_arg)
{
    gpio_pin_set_dt(&led, 1);
    LOG_DBG("light on");

    return GOLIOTH_RPC_OK;
}

static enum golioth_rpc_status on_light_off(zcbor_state_t *request_params_array,
                                            zcbor_state_t *response_detail_map,
                                            void *callback_arg)
{
    gpio_pin_set_dt(&led, 0);
    LOG_DBG("light off");

    return GOLIOTH_RPC_OK;
}

static void on_client_event(struct golioth_client *client,
                            enum golioth_client_event event,
                            void *arg)
{
    bool is_connected = (event == GOLIOTH_CLIENT_EVENT_CONNECTED);
    if (is_connected)
    {
        k_sem_give(&connected);
    }
    LOG_INF("Golioth client %s", is_connected ? "connected" : "disconnected");
}

int main(void)
{
    LOG_DBG("Start tinymcp blinky");

    int err;

    if (!device_is_ready(led.port))
    {
        return -EIO;
    }

    err = gpio_pin_configure_dt(&led, GPIO_OUTPUT_ACTIVE);
    if (err < 0)
    {
        return -EIO;
    }

    net_connect();

    /* Note: In production, credentials should be saved in secure storage. For
     * simplicity, we provide a utility that stores credentials as plaintext
     * settings.
     */
    const struct golioth_client_config *client_config = golioth_sample_credentials_get();

    struct golioth_client *client = golioth_client_create(client_config);
    struct golioth_rpc *rpc = golioth_rpc_init(client);

    golioth_client_register_event_callback(client, on_client_event, NULL);

    k_sem_take(&connected, K_FOREVER);

    err = golioth_rpc_register(rpc, "light_on", on_light_on, NULL);
    if (err)
    {
        LOG_ERR("Failed to register light_on RPC: %d", err);
    }

    err = golioth_rpc_register(rpc, "light_off", on_light_off, NULL);
    if (err)
    {
        LOG_ERR("Failed to register light_off RPC: %d", err);
    }

    while (true)
    {
        k_sleep(K_SECONDS(5));
    }

    return 0;
}

A device running this firmware application would connect to Golioth, then wait for incoming RPCs. The tinymcp MCP server connects to Golioth and queries the device’s LightDB State data to determine what “tools” are exposed. For example, the following state would expose the two RPCs registered in the blinky example.

{
    "mcp": {
        "tools": {
            "schema": [
                {
                    "name": "light_on"
                },
                {
                    "name": "light_off"
                }
            ]
        }
    }
}

Because both users and devices can write to LightDB State, this schema can be uploaded in the console or via the management API to expose existing device RPCs, or the device firmware can be updated to send the schema to LightDB State on boot as a more self-contained option.

The tinymcp server can be registered with any MCP client, such as Claude Code, Cursor, or the Gemini CLI. Natural language can then be used to interact with devices, such as prompting the LLM to “turn the light off”.

What’s Next

Giving LLMs access to the physical world opens up a wide variety of new use cases. While delegating to AI must be done carefully, we are excited to see how the Golioth community is able to creatively leverage this new functionality. We also have a ton of ideas for features that can easily be added to tinymcp, such as multiple device targeting, offloaded tool handlers, and more. Open an issue or reach out to us on the forum to share your ideas!

Dan Mangum
Dan Mangum
Dan is an experienced engineering leader, having built products and teams at both large companies and small startups. He has a history of leadership in open source communities, and has worked across many layers of the technical stack, giving him unique insight into the constraints faced by Golioth’s customers and the requirements of a platform that enables their success.

Post Comments

No comments yet! Start the discussion at forum.golioth.io

More from this author

Related posts

spot_img

Latest posts

Use the ESP32 AT Binary to Make Any Zephyr Project Wi-Fi Enabled

You can add Wi-Fi to any Zephyr project with an ESP32 running the ESP-AT firmware and a couple of configuration symbols in Zephyr. Let the data flow!

The ESP32 HCI makes any Zephyr board a Bluetooth gateway

Bluetooth HCI enables any chipset to act as a peripheral to a processor running a Bluetooth stack. We pair the ESP32-C3 HCI with a processor running Zephyr to turn Golioth's development platform--the Aludel Elixir--into a Bluetooth gateway.

A Remote Shell for Embedded IoT Devices

Golioth's Remote Shell uses Remote Procedure Calls (RPCs) and a custom Zephyr shell backend to enable an interactive, web-based shell experience from anywhere in the world.

Want to stay up to date with the latest news?

Subscribe to our newsletter and get updates every 2 weeks. Follow the latest blogs and industry trends.