# ESP Service

[中文]

## Introduction

ESP Service is a three-layer service infrastructure for ESP-IDF. The service base class provides a common lifecycle state machine and event publishing. The service manager adds runtime registration, batch start and stop, and tool invocation. An optional MCP (Model Context Protocol) server then exposes the tools of registered services to a large language model or agent over several transports. Button, Wi-Fi, CLI, and OTA services are all built on this base class.

## Feature List

- Lifecycle state machine: `UNINITIALIZED` → `INITIALIZED` → `RUNNING` ⇄ `PAUSED`; all state transitions execute synchronously in the caller’s task context

- A vtable-based (`esp_service_ops_t`) subclassing mechanism; a derived service only needs to implement the lifecycle callbacks it requires

- Each service instance is bound to an ADF Event Hub, and publishes/subscribes to events via `esp_service_publish_event()` / `esp_service_event_subscribe()`

- Low-power hooks `on_lowpower_enter` / `on_lowpower_exit`, which do not trigger state transitions

- `esp_service_manager` supports runtime registration/deregistration, lookup by name or category, and batch `start_all` / `stop_all`

- A service registration can carry a JSON-formatted tool description, which the manager automatically parses and exposes for invocation via `esp_service_manager_invoke_tool()`

- The optional MCP server implements the MCP 2024-11-05 protocol (`tools/list`, `tools/call`, `notifications/tools/list_changed`), supporting six transport methods: HTTP, SSE, WebSocket, UART, STDIO, and SDIO

- Both the service manager and the MCP server are internally protected by mutexes, allowing concurrent calls in a multitasking environment

## Technical Deep Dive

### Three-Layer Model

ESP Service is divided into three independent layers that can be adopted as needed: `esp_service_t` is the minimal usable unit, providing lifecycle management and event-publishing capability on its own; `esp_service_manager_t` provides unified registration and lookup across multiple service instances; the MCP server is then mounted on top of the manager to expose tool invocations to external Agents.

            classDiagram
        direction TB
        class esp_service_t {
            +state
            +esp_service_start()
            +esp_service_publish_event()
        }
        class esp_service_manager_t {
            +esp_service_manager_register()
            +esp_service_manager_invoke_tool()
        }
        class esp_service_mcp_server_t {
            +tools/list
            +tools/call
        }
        esp_service_manager_t "1" o-- "0..*" esp_service_t
        esp_service_mcp_server_t --> esp_service_manager_t

A service can be implemented using only the base class; the manager is introduced for multi-service orchestration; whether the MCP server is mounted is entirely optional, and the three layers are not mandatorily bound together.

### Lifecycle and Subclassing

A derived service embeds `esp_service_t` as the first member of its struct, fills in the required callbacks in `esp_service_ops_t`, and then calls `esp_service_init()` to complete initialization. The state machine is maintained by the base class; the four lifecycle APIs (`esp_service_start()`, `esp_service_stop()`, `esp_service_pause()`, `esp_service_resume()`) all synchronously invoke the corresponding `ops` callback in the caller’s task context. If a service requires a long-running background task, it should create the task inside `on_start` and return immediately.

    typedef struct {
        esp_service_t base;  /* Must be the first member */
        /* ... derived fields ... */
    } my_service_t;

    static esp_err_t my_on_start(esp_service_t *base)
    {
        my_service_t *svc = (my_service_t *)base;
        /* Create background task, enable hardware, etc. */
        return ESP_OK;
    }

    static const esp_service_ops_t s_my_ops = {
        .on_start = my_on_start,
    };

    esp_service_config_t cfg = { .name = "my_service" };
    esp_service_init(&svc->base, &cfg, &s_my_ops);

The base class automatically publishes the `ESP_SERVICE_EVENT_STATE_CHANGED` event (with ID `UINT16_MAX - 1`) after every successful state transition; when a derived service defines its own event enumeration, it must not use this value or the wildcard `UINT16_MAX`. Domain events (such as OTA progress or button actions) are published over the same event bus; see ADF Event Hub for the publish/subscribe usage.

Note

The low-power hooks are invoked directly by `esp_service_lowpower_enter()` / `esp_service_lowpower_exit()`, bypassing the state machine, and are suitable for suspending peripheral resources such as radios or LEDs.

### Service Manager

`esp_service_manager_t` maintains a service registry, where each entry is described by `esp_service_registration_t`: a mandatory `service` instance, an optional category string `category`(queried via `find_by_category`), and an optional pair of `tool_desc` / `tool_invoke`. When both are set, the manager parses the JSON tool description array in `tool_desc` and routes calls to `esp_service_manager_invoke_tool()` to the `tool_invoke` callback; when both are empty, only lifecycle management is performed.

    esp_service_manager_t *mgr;
    esp_service_manager_create(NULL, &mgr);

    esp_service_manager_register(mgr, &(esp_service_registration_t){
        .service  = (esp_service_t *)my_service,
        .category = "audio",
    });

    esp_service_manager_start_all(mgr);

A tool description is a JSON array in which each item contains `name`, `description`, and `inputSchema`:

    [
      {
        "name": "player_service_play",
        "description": "Start audio playback",
        "inputSchema": { "type": "object", "properties": {} }
      }
    ]

CLI Service uses the manager to implement its `svc` / `tool` commands; see ESP CLI Service for details.

### MCP Server (Optional)

After enabling `CONFIG_ESP_MCP_ENABLE`, an MCP server can be created to expose the tools registered on the manager to external Agents via JSON-RPC 2.0. The server itself is decoupled from the transport method: `esp_service_manager_as_tool_provider()` wraps the manager as a tool provider, which is then supplied together with a concrete transport instance.

            flowchart TD
        LLM["LLM / AI Agent"] --> MCP[MCP Server]
        MCP --> MGR[Service Manager]
        MGR --> S1[Service A]
        MGR --> S2[Service B]

Each supported transport method corresponds to its own Kconfig option: HTTP (`POST /mcp`), SSE streaming, WebSocket, UART, STDIO, and SDIO, all sharing `esp_service_mcp_trans_t` as a unified interface; one or more can be selected depending on the target device’s connectivity.

    esp_service_mcp_trans_t *transport = NULL;
    esp_service_mcp_trans_http_create(&http_cfg, &transport);

    esp_service_mcp_server_config_t cfg = ESP_SERVICE_MCP_SERVER_CONFIG_DEFAULT();
    esp_service_manager_as_tool_provider(mgr, &cfg.tool_provider);
    cfg.transport = transport;

    esp_service_mcp_server_t *server = NULL;
    esp_service_mcp_server_create(&cfg, &server);
    esp_service_mcp_server_start(server);

## Application Examples

- `components/esp_service/examples/mock_services/` demonstrates the combination of the service manager with all MCP transport methods, along with a host-side Python test script.

- services_hub demonstrates a combination of multiple services—Wi-Fi Service, OTA Service, CLI Service, and Button Service—integrated with `esp_board_manager` in a production-style usage.

## FAQ

Q1: Must a service be registered with esp_service_manager to be used?

No. `esp_service_t` on its own is sufficient for initialization, start/stop, and event publishing; the manager is only needed when cross-service orchestration or MCP tool invocation is required.

Q2: Can the MCP server enable multiple transport methods at the same time?

Each `esp_service_mcp_server_t` instance is bound to a single transport instance; when multiple transport methods need to be provided simultaneously, create multiple server instances that share the same tool provider.

## API Reference

### Header File

- components/esp_service/include/esp_service.h

### Functions

esp_err_t esp_service_init(esp_service_t *service, const esp_service_config_t *config, const esp_service_ops_t *ops)

  Initialize service base.

  Parameters:

    - service – [in] Service instance (caller allocates)

    - config – [in] Configuration

    - ops – [in] Lifecycle operations (may be NULL)

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG service or config is NULL.

    - ESP_ERR_NO_MEM Insufficient memory.

    - other Error from ops->on_init.

esp_err_t esp_service_deinit(esp_service_t *service)

  Deinitialize service base.

  Parameters:

    service – [in] Service instance

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Invalid argument

esp_err_t esp_service_start(esp_service_t *service)

  Start service (INITIALIZED -> RUNNING)

          Calls ops->on_start() synchronously in the caller's task context.
          If the service needs background work it should create its own task
          inside on_start and return immediately.

  Parameters:

    service – [in] Service instance

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Invalid argument

    - ESP_ERR_INVALID_STATE Not in INITIALIZED state (e.g. already RUNNING)

    - other Error returned by ops->on_start

esp_err_t esp_service_stop(esp_service_t *service)

  Stop service (any -> INITIALIZED)

  Parameters:

    service – [in] Service instance

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Invalid argument

esp_err_t esp_service_pause(esp_service_t *service)

  Pause service (RUNNING -> PAUSED)

  Parameters:

    service – [in] Service instance

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG service is NULL.

    - ESP_ERR_INVALID_STATE Not in RUNNING state.

    - other Error from ops->on_pause.

esp_err_t esp_service_resume(esp_service_t *service)

  Resume service (PAUSED -> RUNNING)

  Parameters:

    service – [in] Service instance

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG service is NULL.

    - ESP_ERR_INVALID_STATE Not in PAUSED state.

    - other Error from ops->on_resume.

esp_err_t esp_service_get_state(const esp_service_t *service, esp_service_state_t *out_state)

  Get current service state.

  Parameters:

    - service – [in] Service instance

    - out_state – [out] Output: current state

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Invalid argument

esp_err_t esp_service_is_running(const esp_service_t *service, bool *out_running)

  Check if service is running.

  Parameters:

    - service – [in] Service instance

    - out_running – [out] Output: true if state is RUNNING

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Invalid argument

esp_err_t esp_service_publish_event(esp_service_t *service, uint16_t event_id, const void *payload, uint16_t payload_len, adf_event_payload_release_cb_t release_cb, void *release_ctx)

  Publish a domain event through the service-bound Event Hub.

  Note

  Payload ownership contract (stricter than adf_event_hub_publish()):

  - If release_cb is non-NULL, it is invoked EXACTLY ONCE for every invocation of this function, regardless of the return code (ESP_OK, INVALID_ARG, INVALID_STATE, NOT_FOUND, NO_MEM).

  - The caller MUST NOT free or otherwise access payload after this function returns; doing so results in a double-free / UAF.

  - release_cb may fire synchronously inside this call (callback-mode delivery or early error path), so any fields the caller needs to log/use must be captured into local variables BEFORE the call.

  - If release_cb is NULL, the caller keeps payload ownership on all paths (typical for stack/static/long-lived payloads).

  Parameters:

    - service – [in] Service instance

    - event_id – [in] Domain-local event id (must be > 0)

    - payload – [in] Event payload pointer (may be NULL)

    - payload_len – [in] Payload length in bytes

    - release_cb – [in] Optional payload release callback; when non-NULL, ownership of payload is transferred on every path

    - release_ctx – [in] Optional release callback context

  Returns:

    - ESP_OK Event published.

    - ESP_ERR_INVALID_ARG service is NULL or event_id is zero.

    - ESP_ERR_INVALID_STATE No event hub bound.

    - ESP_ERR_NOT_FOUND Hub is not a registered domain.

    - ESP_ERR_NO_MEM Internal envelope allocation failed.

esp_err_t esp_service_event_subscribe(esp_service_t *service, const adf_event_subscribe_info_t *info)

  Subscribe to events on a domain through the service-bound Event Hub.

  Note

  Wraps adf_event_hub_subscribe(). At least one delivery target (target_queue or handler) must be set in info.

  Parameters:

    - service – [in] Service instance (must have a bound event hub)

    - info – [in] Subscription parameters

  Returns:

    - ESP_OK Subscriber registered.

    - ESP_ERR_INVALID_ARG service or info is NULL.

    - ESP_ERR_INVALID_STATE No event hub bound.

    - ESP_ERR_NO_MEM Subscriber storage allocation failed.

esp_err_t esp_service_event_unsubscribe(esp_service_t *service, const char *domain, uint16_t event_id)

  Unsubscribe from events on a domain.

  Parameters:

    - service – [in] Service instance (must have a bound event hub)

    - domain – [in] Domain to unsubscribe from; NULL = service’s own domain

    - event_id – [in] Event ID filter; ADF_EVENT_ANY_ID removes all

  Returns:

    - ESP_OK At least one subscriber removed.

    - ESP_ERR_INVALID_ARG service is NULL.

    - ESP_ERR_INVALID_STATE No event hub bound.

    - ESP_ERR_NOT_FOUND Domain not registered or no matching subscriber.

esp_err_t esp_service_event_delivery_done(esp_service_t *service, adf_event_delivery_t *delivery)

  Release a queue-mode delivery reference.

  Note

  Must be called exactly once per received adf_event_delivery_t.

  Parameters:

    - service – [in] Service instance (must have a bound event hub)

    - delivery – [in] Delivery item from the subscriber queue

  Returns:

    - ESP_OK Reference released.

    - ESP_ERR_INVALID_ARG service or delivery is NULL.

    - ESP_ERR_INVALID_STATE No event hub bound.

esp_err_t esp_service_get_event_hub(const esp_service_t *service, adf_event_hub_t *out_hub)

  Get the event hub handle bound to a service.

  Parameters:

    - service – [in] Service instance

    - out_hub – [out] Output: event hub handle (NULL if not bound)

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG service or out_hub is NULL.

esp_err_t esp_service_get_last_error(const esp_service_t *service, esp_err_t *out_err)

  Get last error recorded by the base layer (e.g. on_start failed)

          Returns the error code from the most recent failed lifecycle call.
          ESP_OK if no error has occurred.

  Parameters:

    - service – [in] Service instance

    - out_err – [out] Output: last error code (ESP_OK if none)

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Invalid argument

esp_err_t esp_service_lowpower_enter(esp_service_t *service)

  Notify service to enter low-power mode.

          Calls ops->on_lowpower_enter if provided. This is a direct callback
          invocation — it does not change the service lifecycle state.
          Typical use: suspend radio, turn off LED, reduce clock, etc.

  Parameters:

    service – [in] Service instance

  Returns:

    - ESP_OK On success or on_lowpower_enter is NULL

    - ESP_ERR_INVALID_ARG service is NULL

    - other Error returned by on_lowpower_enter

esp_err_t esp_service_lowpower_exit(esp_service_t *service)

  Notify service to exit low-power mode.

          Calls ops->on_lowpower_exit if provided. This is a direct callback
          invocation — it does not change the service lifecycle state.

  Parameters:

    service – [in] Service instance

  Returns:

    - ESP_OK On success or on_lowpower_exit is NULL

    - ESP_ERR_INVALID_ARG service is NULL

    - other Error returned by on_lowpower_exit

esp_err_t esp_service_get_name(const esp_service_t *service, const char **out_name)

  Get service name.

  Parameters:

    - service – [in] Service instance

    - out_name – [out] Output: service name pointer

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Invalid argument

esp_err_t esp_service_get_user_data(const esp_service_t *service, void **out_data)

  Get user data.

  Parameters:

    - service – [in] Service instance

    - out_data – [out] Output: user data pointer

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Invalid argument

esp_err_t esp_service_set_user_data(esp_service_t *service, void *user_data)

  Replace the user-data pointer held by the base layer.

          Typical use: a subclass clears `user_data` in `on_deinit` after freeing
          its private context. Does not free the previous pointer.

  Parameters:

    - service – [in] Service instance

    - user_data – [in] New value (may be NULL)

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG service is NULL

esp_err_t esp_service_set_event_hub(esp_service_t *service, adf_event_hub_t hub)

  Replace the Event Hub handle bound to the service.

          Assigns the internal hub pointer only. The caller owns hub lifetime:
          destroy or retain any previous handle (e.g. via `esp_service_get_event_hub`)
          before calling, as this function does not call `adf_event_hub_destroy`.

  Parameters:

    - service – [in] Service instance

    - hub – [in] Hub handle to store (may be NULL)

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG service is NULL

esp_err_t esp_service_state_to_str(esp_service_state_t state, const char **out_str)

  Convert state to string.

  Parameters:

    - state – [in] Service state

    - out_str – [out] Output: state name string

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Invalid argument or state

esp_err_t esp_service_get_event_name(const esp_service_t *service, uint16_t event_id, const char **out_name)

  Get human-readable name for a domain event ID.

  Note

  The base STATE_CHANGED event is handled by the core layer. Domain-specific events are delegated to ops->event_to_name if provided.

  Parameters:

    - service – [in] Service instance

    - event_id – [in] Event ID to look up

    - out_name – [out] Output: event name string (may be NULL if unknown)

  Returns:

    - ESP_OK Name found

    - ESP_ERR_INVALID_ARG Invalid argument

    - ESP_ERR_NOT_FOUND No name mapping for this event ID

### Structures

struct esp_service_state_changed_payload_t

  Payload for ESP_SERVICE_EVENT_STATE_CHANGED.

  Public Members

  esp_service_state_t old_state

    Previous state

  esp_service_state_t new_state

    New state

struct esp_service_config_t

  Service base configuration.

  Public Members

  const char *name

    Service name (required); also used as event hub domain

  void *user_data

    User data passed to callbacks

struct esp_service_ops

  Lifecycle operations (virtual methods for derived services)

          These are called by the base class at appropriate lifecycle points.
          Derived services implement these to add custom behavior.
          All methods are optional (NULL = no-op).

  Public Members

  esp_err_t (*on_init)(esp_service_t *service, const esp_service_config_t *config)

    Called during init

  esp_err_t (*on_deinit)(esp_service_t *service)

    Called during deinit

  esp_err_t (*on_start)(esp_service_t *service)

    Called when starting

  esp_err_t (*on_stop)(esp_service_t *service)

    Called when stopping

  esp_err_t (*on_pause)(esp_service_t *service)

    Called when pausing

  esp_err_t (*on_resume)(esp_service_t *service)

    Called when resuming

  esp_err_t (*on_lowpower_enter)(esp_service_t *service)

    Low-power enter hook; suspend resources

  esp_err_t (*on_lowpower_exit)(esp_service_t *service)

    Low-power exit hook; restore resources

  const char *(*event_to_name)(uint16_t event_id)

    Map event_id to name; NULL if unknown

struct esp_service

  Service base structure.

          Derived services embed this as the first member:

      typedef struct {
          esp_service_t base;  // Must be first
          // ... derived fields
      } my_service_t;

  Public Members

  const char *name

    Service name (owned copy); also used as event hub domain

  esp_service_state_t state

    Current state; atomic — safe to read from any task without a lock

  const esp_service_ops_t *ops

    Lifecycle operations vtable

  void *user_data

    User context (from config, for service logic)

  esp_err_t last_err

    Last error recorded by the base layer

  adf_event_hub_t event_hub

    Optional Event Hub handle (from adf_event_hub_create)

### Macros

ESP_SERVICE_EVENT_STATE_CHANGED

  Service base module.

          This is the base class for all services. It provides:
          - Lifecycle operations via esp_service_ops_t (virtual methods)
          - State machine: UNINITIALIZED -> INITIALIZED -> RUNNING <-> PAUSED
          - All lifecycle API calls (start/stop/pause/resume) execute synchronously
            in the caller's task context, invoking the corresponding vtable op.
            Services that need a persistent background task create and manage it
            themselves inside on_init / on_start / on_stop.
          - Low-power hooks: optional on_lowpower_enter / on_lowpower_exit ops; invoked by
            esp_service_lowpower_enter() / esp_service_lowpower_exit(), which are simple
            direct calls — no state change, no SLEEPING state.
          - Event hub integration: optional adf_event_hub for publish-subscribe events
          - All functions return esp_err_t, results passed via output parameters

          Subclass domain events (e.g. player track end, OTA progress) are not
          part of the base; each subclass defines its own event enum, payload,
          and set_event_cb. See docs/SUBCLASS_EVENT_DESIGN.md for the pattern.

  Event IDs published by the service base layer.

          These are published automatically when a service has a bound event hub.
          Subscribers use these IDs in adf_event_subscribe_info_t::event_id.

          Base-layer IDs occupy the top of the uint16_t range so that the low
          range [1 .. UINT16_MAX-2] is freely available for domain-specific events.
          ADF_EVENT_ANY_ID (UINT16_MAX) is the wildcard; derived services MUST NOT
          use UINT16_MAX or UINT16_MAX-1.

  State transition; payload: esp_service_state_changed_payload_t

ESP_SERVICE_BASE(service)

  Get base service.

ESP_SERVICE_CONFIG_DEFAULT()

  Default configuration macro.

### Type Definitions

typedef struct esp_service esp_service_t

typedef struct esp_service_ops esp_service_ops_t

  Lifecycle operations (virtual methods for derived services)

          These are called by the base class at appropriate lifecycle points.
          Derived services implement these to add custom behavior.
          All methods are optional (NULL = no-op).

### Enumerations

enum esp_service_state_t

  Service state enumeration.

  Values:

  enumerator ESP_SERVICE_STATE_UNINITIALIZED

    Not initialized

  enumerator ESP_SERVICE_STATE_INITIALIZED

    Initialized, ready to start

  enumerator ESP_SERVICE_STATE_RUNNING

    Running normally

  enumerator ESP_SERVICE_STATE_PAUSED

    Paused, can resume

  enumerator ESP_SERVICE_STATE_STOPPING

    Stopping (internal)

  enumerator ESP_SERVICE_STATE_ERROR

    Error state

  enumerator ESP_SERVICE_STATE_MAX

    Sentinel, do not use

### Header File

- components/esp_service/include/esp_service_manager.h

### Functions

esp_err_t esp_service_manager_create(const esp_service_manager_config_t *config, esp_service_manager_t **out_mgr)

  Create service manager.

  Parameters:

    - config – [in] Configuration (NULL for defaults)

    - out_mgr – [out] Output: manager instance

  Returns:

    - ESP_OK On success

    - ESP_ERR_NO_MEM Allocation failed

    - ESP_ERR_INVALID_ARG out_mgr is NULL

esp_err_t esp_service_manager_destroy(esp_service_manager_t *mgr)

  Destroy service manager.

  Parameters:

    mgr – [in] Manager instance

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG mgr is NULL.

esp_err_t esp_service_manager_register(esp_service_manager_t *mgr, const esp_service_registration_t *reg)

  Register a service.

          Automatically parses tool description from service->tool_desc and
          registers all capabilities as invocable tools.

  Parameters:

    - mgr – [in] Manager instance

    - reg – [in] Registration info

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG Parameters invalid

    - ESP_ERR_NO_MEM Registry full

    - ESP_FAIL JSON parsing failed

esp_err_t esp_service_manager_unregister(esp_service_manager_t *mgr, esp_service_t *service)

  Unregister a service.

  Parameters:

    - mgr – [in] Manager instance

    - service – [in] Service to unregister

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG mgr or service is NULL.

    - ESP_ERR_NOT_FOUND Service not registered.

    - ESP_ERR_TIMEOUT Mutex acquisition timed out.

esp_err_t esp_service_manager_find_by_name(esp_service_manager_t *mgr, const char *name, esp_service_t **out_service)

  Find service by name.

  Parameters:

    - mgr – [in] Manager instance

    - name – [in] Service name

    - out_service – [out] Output: service instance

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG mgr, name, or out_service is NULL.

    - ESP_ERR_NOT_FOUND Service not found.

    - ESP_ERR_TIMEOUT Mutex acquisition timed out.

esp_err_t esp_service_manager_find_by_category(esp_service_manager_t *mgr, const char *category, uint16_t index, esp_service_t **out_service)

  Find service by category.

  Parameters:

    - mgr – [in] Manager instance

    - category – [in] Category string

    - index – [in] Index in category (0-based)

    - out_service – [out] Output: service instance

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG mgr, category, or out_service is NULL.

    - ESP_ERR_NOT_FOUND Service not found.

    - ESP_ERR_TIMEOUT Mutex acquisition timed out.

esp_err_t esp_service_manager_get_count(esp_service_manager_t *mgr, uint16_t *out_count)

  Get number of registered services.

  Parameters:

    - mgr – [in] Manager instance

    - out_count – [out] Output: service count

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG mgr or out_count is NULL.

    - ESP_ERR_TIMEOUT Mutex acquisition timed out.

esp_err_t esp_service_manager_start_all(esp_service_manager_t *mgr)

  Start all registered services.

  Parameters:

    mgr – [in] Manager instance

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG mgr is NULL.

    - ESP_ERR_TIMEOUT Mutex acquisition timed out.

esp_err_t esp_service_manager_stop_all(esp_service_manager_t *mgr)

  Stop all registered services.

  Parameters:

    mgr – [in] Manager instance

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG mgr is NULL.

    - ESP_ERR_TIMEOUT Mutex acquisition timed out.

esp_err_t esp_service_manager_get_tools(esp_service_manager_t *mgr, const esp_service_tool_t **out_tools, uint16_t max_tools, uint16_t *out_count)

  Get all available tools.

  Parameters:

    - mgr – [in] Manager instance

    - out_tools – [out] Output: array of tool pointers

    - max_tools – [in] Size of out_tools array

    - out_count – [out] Output: actual tool count

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG mgr, out_tools, or out_count is NULL.

    - ESP_ERR_TIMEOUT Mutex acquisition timed out.

esp_err_t esp_service_manager_clone_tools(esp_service_manager_t *mgr, esp_service_tool_t **out_tools, uint16_t *out_count)

  Get a deep-copied snapshot of all registered tools.

          Unlike esp_service_manager_get_tools(), which returns raw pointers
          into manager-owned storage that are invalidated on unregister, this
          function allocates a caller-owned array whose name/description/
          input_schema strings are duplicated under the manager mutex, so the
          snapshot stays valid across concurrent service unregistration.
          Free the result with esp_service_manager_free_cloned_tools().

  Parameters:

    - mgr – [in] Manager instance

    - out_tools – [out] Receives a newly allocated array (NULL when count==0)

    - out_count – [out] Receives the number of entries in *out_tools

  Returns:

    - ESP_OK On success (including the empty-registry case).

    - ESP_ERR_INVALID_ARG mgr, out_tools, or out_count is NULL.

    - ESP_ERR_TIMEOUT Mutex acquisition timed out.

    - ESP_ERR_NO_MEM Allocation failed.

void esp_service_manager_free_cloned_tools(esp_service_tool_t *tools, uint16_t count)

  Free an array returned by esp_service_manager_clone_tools()

  Parameters:

    - tools – [in] Array to free (may be NULL)

    - count – [in] Number of entries in the array

esp_err_t esp_service_manager_invoke_tool(esp_service_manager_t *mgr, const char *tool_name, const char *args_json, char *result, size_t result_size)

  Invoke a tool.

          The manager mutex is released before the handler runs, so
          long-running handlers do not block other manager APIs.

  Note

  The caller must not unregister the owning service while a tool on it is being invoked; the handler receives a shallow snapshot whose string fields are owned by the service entry.

  Parameters:

    - mgr – [in] Manager instance

    - tool_name – [in] Tool name

    - args_json – [in] Arguments (JSON string)

    - result – [out] Buffer for result (JSON string)

    - result_size – [in] Size of result buffer

  Returns:

    - ESP_OK On success.

    - ESP_ERR_INVALID_ARG mgr, tool_name, or result is NULL.

    - ESP_ERR_NOT_FOUND Tool not found.

    - ESP_ERR_NOT_SUPPORTED Tool has no invocation handler.

    - ESP_ERR_TIMEOUT Mutex acquisition timed out.

    - Other Error from tool invocation handler.

esp_err_t esp_service_manager_as_tool_provider(esp_service_manager_t *mgr, esp_service_mcp_tool_provider_t *out_provider)

  Wrap a service manager instance as an esp_service_mcp_tool_provider_t.

          Populates out_provider with callbacks that delegate to the given
          manager.  No allocation is performed; out_provider may live on the
          stack or inside a config struct.

  Parameters:

    - mgr – [in] Service manager instance

    - out_provider – [out] Provider struct to populate

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG mgr or out_provider is NULL

### Structures

struct esp_service_registration_t

  Service registration info.

          tool_desc and tool_invoke are both optional:
          - Both NULL  → service is registered for lifecycle management only;
                         no tool discovery or dispatch is set up.
          - Both set   → manager parses tool_desc and routes invoke_tool() calls
                         through tool_invoke; enables MCP tool support.

  Public Members

  esp_service_t *service

    Service instance

  const char *category

    Service category (e.g., “audio”, “display”)

  uint32_t flags

    Combination of ESP_SERVICE_REG_FLAG_xxx (0 = default)

  const char *tool_desc

    MCP tool description JSON array; NULL = no tool support

  esp_service_tool_invoke_fn_t tool_invoke

    Tool invocation handler; NULL = no tool support

struct esp_service_manager_config_t

  Service manager configuration.

  Public Members

  uint16_t max_services

    Maximum number of services (default: 16)

  uint16_t max_tools_per_service

    Max tools per service (default: 32)

  bool auto_start_services

    Auto-start services on registration

### Macros

ESP_SERVICE_REG_FLAG_SKIP_BATCH_START

  Registration flags (per-service batch control)

  Skip this service in start_all

ESP_SERVICE_REG_FLAG_SKIP_BATCH_STOP

  Skip this service in stop_all

ESP_SERVICE_MANAGER_CONFIG_DEFAULT()

  Default configuration.

### Type Definitions

typedef struct esp_service_manager esp_service_manager_t

  Service Manager - Dynamic service registry and lifecycle management.

          Features:
          - Dynamic service registration/unregistration
          - Automatic capability discovery from service JSON schemas
          - Service lifecycle management (init/start/stop/deinit)
          - Query services by name or category
          - Thread-safe operations

typedef esp_err_t (*esp_service_tool_invoke_fn_t)(esp_service_t *service, const esp_service_tool_t *tool, const char *args, char *result, size_t result_size)

  Tool invocation callback — translate a JSON-RPC tool call into a C API call.

  Param service:

    [in] Service instance (cast to concrete type inside)

  Param tool:

    [in] Tool metadata (name, description, input_schema)

  Param args:

    [in] JSON string with tool arguments (may be NULL or “{}”)

  Param result:

    [out] Buffer to write JSON result string into

  Param result_size:

    [in] Size of result buffer

  Return:

    - ESP_OK On success

    - ESP_ERR_NOT_SUPPORTED Unknown tool name

    - ESP_ERR_INVALID_ARG Missing or invalid argument

### Header File

- components/esp_service/include/esp_service_mcp_server.h

### Functions

esp_err_t esp_service_mcp_server_create(const esp_service_mcp_server_config_t *config, esp_service_mcp_server_t **out_srv)

  Create MCP server.

          Binds to the given transport by setting a request handler callback.
          The server does NOT take ownership of the transport; the caller is
          responsible for destroying it after the server is destroyed.

  Parameters:

    - config – [in] Configuration (tool_provider callbacks and transport are required)

    - out_srv – [out] Output: server instance

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG config, tool_provider callbacks, or transport is NULL

    - ESP_ERR_NO_MEM Allocation failed

esp_err_t esp_service_mcp_server_destroy(esp_service_mcp_server_t *srv)

  Destroy MCP server.

          Stops the server if still running. Does NOT destroy the transport.

  Parameters:

    srv – [in] Server instance

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG If srv is NULL

esp_err_t esp_service_mcp_server_start(esp_service_mcp_server_t *srv)

  Start MCP server.

          Starts the bound transport, which begins accepting client connections/data.

  Parameters:

    srv – [in] Server instance

  Returns:

    - ESP_OK On success (including when the server is already running)

    - ESP_ERR_INVALID_ARG If srv is NULL

    - Other Error code returned by the bound transport’s start routine

esp_err_t esp_service_mcp_server_stop(esp_service_mcp_server_t *srv)

  Stop MCP server.

          Stops the bound transport.

  Parameters:

    srv – [in] Server instance

  Returns:

    - ESP_OK On success (including when the server is not running)

    - ESP_ERR_INVALID_ARG If srv is NULL

esp_err_t esp_service_mcp_server_handle_request(esp_service_mcp_server_t *srv, const char *request, esp_service_mcp_response_t *response)

  Handle MCP request (synchronous, public API)

          Parses a JSON-RPC request and dispatches to the appropriate handler.
          Protocol-level failures (parse error, unknown method, invalid params) are
          reported in the returned response structure, not via the return code.

  Parameters:

    - srv – [in] Server instance

    - request – [in] JSON-RPC request string

    - response – [out] Output: response structure (caller must call esp_service_mcp_response_free)

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG If srv, request or response is NULL

esp_err_t esp_service_mcp_server_notify(esp_service_mcp_server_t *srv, const char *method, const char *params)

  Send notification to all connected clients.

          Broadcasts a JSON-RPC notification via the bound transport.

  Parameters:

    - srv – [in] Server instance

    - method – [in] Notification method (e.g., “notifications/tools/list_changed”)

    - params – [in] Notification params (JSON string, can be NULL)

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG If srv or method is NULL

    - ESP_ERR_NO_MEM If notification serialization fails

    - Other Error code returned by the bound transport’s broadcast routine

esp_err_t esp_service_mcp_server_get_capabilities(esp_service_mcp_server_t *srv, esp_service_mcp_server_capabilities_t *out_caps)

  Get server capabilities.

  Parameters:

    - srv – [in] Server instance

    - out_caps – [out] Output: capabilities

  Returns:

    - ESP_OK On success

    - ESP_ERR_INVALID_ARG If srv or out_caps is NULL

void esp_service_mcp_response_free(esp_service_mcp_response_t *response)

  Free response structure.

  Parameters:

    response – [in] Response to free

char *esp_service_mcp_build_response(const char *id, const char *result)

  Helper: Build JSON-RPC success response.

  Parameters:

    - id – [in] Request ID (JSON string or number)

    - result – [in] Result object (JSON string)

  Returns:

    - Non-NULL Heap-allocated JSON-RPC response string; caller must free()

    - NULL On allocation failure (OOM)

char *esp_service_mcp_build_error(const char *id, int code, const char *message)

  Helper: Build JSON-RPC error response.

  Parameters:

    - id – [in] Request ID (can be NULL)

    - code – [in] Error code

    - message – [in] Error message

  Returns:

    - Non-NULL Heap-allocated JSON-RPC error response string; caller must free()

    - NULL On allocation failure (OOM)

char *esp_service_mcp_build_tools_list(const esp_service_tool_t **tools, uint16_t count)

  Helper: Build tools/list response.

  Parameters:

    - tools – [in] Array of tools

    - count – [in] Tool count

  Returns:

    - Non-NULL Heap-allocated JSON array string; caller must free()

    - NULL On allocation failure (OOM)

### Structures

struct esp_service_tool

  MCP tool descriptor.

          Populated at registration time by parsing the tool_desc JSON.
          Fields are read-only from the MCP server's perspective.

  Public Members

  char *name

    Tool name

  char *description

    Tool description

  char *input_schema

    Input schema (JSON object string)

struct esp_service_mcp_tool_provider_t

  Abstract tool provider interface.

          Decouples esp_service_mcp_server from any concrete tool registry.
          Implement these two callbacks to expose tools to the MCP server.
          Use esp_service_manager_as_tool_provider() to wrap an
          esp_service_manager_t instance.

  Public Members

  esp_err_t (*get_tools)(void *ctx, const esp_service_tool_t **out_tools, uint16_t max_tools, uint16_t *out_count)

    Enumerate all available tools.

    Param ctx:

      [in] Opaque context

    Param out_tools:

      [out] Array to fill with tool pointers

    Param max_tools:

      [in] Capacity of out_tools

    Param out_count:

      [out] Number of tools written

    Return:

      - ESP_OK On success

  esp_err_t (*invoke_tool)(void *ctx, const char *tool_name, const char *args_json, char *result, size_t result_size)

    Invoke a named tool.

    Param ctx:

      [in] Opaque context

    Param tool_name:

      [in] Tool name

    Param args_json:

      [in] JSON-encoded arguments (may be NULL or “{}”)

    Param result:

      [out] Buffer for JSON result string

    Param result_size:

      [in] Size of result buffer

    Return:

      - ESP_OK On success

      - ESP_ERR_NOT_FOUND If the tool name is unknown

  void *ctx

    Opaque context passed to both callbacks

struct esp_service_mcp_response_t

  MCP request handler result.

  Public Members

  char *response

    JSON-RPC response string (allocated)

  bool is_notification

    True if no response needed

  esp_err_t error

    Error code

struct esp_service_mcp_server_config_t

  MCP server configuration.

          No transport-specific fields. All transport configuration
          is handled by the concrete transport's own create function.

  Public Members

  esp_service_mcp_tool_provider_t tool_provider

    Tool source (esp_service_manager_as_tool_provider())

  esp_service_mcp_trans_t *transport

    Abstract transport instance (required)

  const char *server_name

    Server name for MCP initialize (default: “esp-mcp-server”)

  const char *server_version

    Server version for MCP initialize (default: “1.0.0”)

  size_t max_request_size

    Max request size in bytes (default: 4096)

  size_t max_response_size

    Max tool result + response size in bytes (default: 4096)

  uint16_t max_tools

    Max tools for tools/list (default: 64)

  uint32_t timeout_ms

    Request timeout (default: 5000)

struct esp_service_mcp_server_capabilities_t

  MCP server capabilities.

  Public Members

  bool tools

    Supports tools

  bool tools_list_changed

    Supports list_changed notification

### Macros

ESP_SERVICE_MCP_SERVER_CONFIG_DEFAULT()

  Default server configuration.

### Type Definitions

typedef struct esp_service_mcp_server esp_service_mcp_server_t

  MCP Server - Exposes services as Model Context Protocol tools.

          Implements MCP specification 2024-11-05:
          - tools/list: List available tools
          - tools/call: Invoke tools
          - notifications/tools/list_changed: Notify tool list changes

          Architecture:
          - Transport-agnostic: receives a pre-created esp_service_mcp_trans_t*
            and communicates through the abstract transport interface.
          - Tool-source-agnostic: receives an esp_service_mcp_tool_provider_t that
            abstracts the underlying tool registry; use
            esp_service_manager_as_tool_provider() to wrap an
            esp_service_manager_t, or supply custom callbacks.
          - JSON-RPC 2.0 protocol handling
          - Request/response validation

          Typical usage:

      // 1. Create a concrete transport
      esp_service_mcp_trans_t *transport = NULL;
      esp_service_mcp_trans_http_create(&http_cfg, &transport);

      // 2. Wrap the service manager as a tool provider
      esp_service_mcp_server_config_t cfg = ESP_SERVICE_MCP_SERVER_CONFIG_DEFAULT();
      esp_service_manager_as_tool_provider(mgr, &cfg.tool_provider);
      cfg.transport = transport;
      esp_service_mcp_server_create(&cfg, &server);

      // 3. Start (starts both server logic and transport)
      esp_service_mcp_server_start(server);

      // 4. Cleanup
      esp_service_mcp_server_stop(server);
      esp_service_mcp_server_destroy(server);
      esp_service_mcp_trans_destroy(transport);

typedef struct esp_service_tool esp_service_tool_t

Provide feedback about this document