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.
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!
No comments yet! Start the discussion at forum.golioth.io