Core
Agent IA v2.x
2

Integration Guide #

Kuzzle IA Agent is a conversational orchestrator that lets users interact with a Kuzzle IoT Platform deployment in natural language. It is composed of a Kuzzle plugin, a Python agent service and an MCP tool server, and is integrated into an existing stack in five steps.

Prerequisites #

  • A running Kuzzle IoT Platform stack with the device-manager, multi-tenancy and keycloak plugins enabled.
  • Node.js and npm matching the versions declared in apps/api/package.json.
  • Python 3.11 and uv if the services run outside Docker.
  • Docker and Docker Compose.
  • An OPENAI_API_KEY. The agent currently runs on OpenAI (gpt-4.1-mini); no other provider is wired in yet.
  • Access to your private npm registry if Kuzzle Enterprise packages are used.

Step 1 — Enable the Kuzzle plugin #

The plugin kuzzle-plugin-ag-ui (packages/kuzzle-plugin-ag-ui/) is loaded by the Kuzzle application alongside the other IoT Platform plugins. On boot, it:

  • registers the agent:run HTTP route (POST /_/agent/run),
  • creates a Kuzzle role named agent.runner and attaches it to every profile suffixed with -tenant_admin or -tenant_reader.

The role provisioning is scheduled ten seconds after boot; if no tenant profile exists yet, it retries every fifteen seconds until at least one matches. If the environment uses a custom profile naming convention, the auto-attachment does not match and the agent.runner role must be granted to the relevant profiles manually.

If the stack is slow to boot, the Kuzzle plugin init timeout can be bumped in apps/api/environments/<env>/kuzzlerc:

{
  "plugins": {
    "common": {
      "initTimeout": 30000
    }
  }
}

Step 2 — Deploy the Python services #

Two containers make up the runtime: the agent (kuzzle-ai-agent) and the MCP tool server (kuzzle-iot-mcp-server). Both are built from their respective Dockerfile.runner.

services:
  kuzzle-iot-mcp-server:
    build:
      context: ./services/kuzzle-iot-mcp-server
      dockerfile: Dockerfile.runner
    command: uv run python app/main.py
    environment:
      - MCP_PACKS=
      - KUZZLE_URL=http://kuzzle-api:7512

  kuzzle-ai-agent:
    build:
      context: ./services/kuzzle-ai-agent
      dockerfile: Dockerfile.runner
    command: uv run uvicorn app.main:app --host 0.0.0.0 --port 8000
    ports:
      - "8000:8000"
    environment:
      - KUZZLE_AGENT_PACKS=
      - MCP_URL=http://kuzzle-iot-mcp-server:5000/mcp
      - OPENAI_API_KEY=${OPENAI_API_KEY}
      - USE_MEMORY_SAVER=true

The plugin reads the agent URL from the AGENT_API_URL environment variable (default: http://kuzzle-ai-agent:8000). It must be set on the kuzzle-api container so it points to the agent service:

services:
  kuzzle-api:
    environment:
      - AGENT_API_URL=http://kuzzle-ai-agent:8000

Step 3 — Configure the front-end #

The chat box is shipped as a Vue component (ChatLauncher) by the package @kuzzleio/vue-plugin-kuzzle-iot-chatbox. Install it from your npm registry:

npm install @kuzzleio/vue-plugin-kuzzle-iot-chatbox

It is then mounted in the application layout, alongside the KLayout component provided by @kuzzleio/iot-platform-frontend:

<template>
  <KLayout :navbar-items="navbarItems" :sidebar-items="sidebarItems" />
  <ChatLauncher class="tw:fixed tw:top-[0.33em] tw:right-36 tw:z-[51]" />
</template>

<script setup lang="ts">
import { KLayout } from '@kuzzleio/iot-platform-frontend';
import { ChatLauncher } from '@kuzzleio/vue-plugin-kuzzle-iot-chatbox';
</script>

The launcher streams the chat conversation to the agent:run endpoint over Server-Sent Events (via an HttpAgent from @ag-ui/client), authenticated with the logged-in user's Kuzzle token. No additional configuration is required when the user is logged in.

Step 4 — Register a pack #

The agent boots with no capability. Features are added as packs, made of two complementary halves: an MCP pack declaring the tools, and an agent pack declaring the intents that call them. A pack may ship only one half when the other is not needed.

MCP side — declare the tools #

An MCP pack is a Python module exposing a PACK: Pack attribute whose mcp_tools lists plain functions. Each tool is registered onto the MCP server, namespaced with the pack id (hello_say_hello).

# app/packs/builtin/hello.py
from app.packs.protocols import Pack


def say_hello(name: str | None = None, kuzzle_token: str | None = None) -> dict:
    """Renvoie un hello world."""
    return {"message": f"Hello {name or 'world'}!"}


PACK = Pack(
    id="hello",
    description="Demo pack: validates MCP tool registration from a pack.",
    mcp_tools=[say_hello],
)

Declare it on the MCP server:

MCP_PACKS=app.packs.builtin.hello

At boot the server logs:

[packs] registered tool 'hello_say_hello' from app.packs.builtin.hello

Agent side — declare the intents #

An agent pack exposes a PACK: Pack whose intents describe how the LLM recognises a request and which handler runs it. A handler calls an MCP tool with tool_invoke and parses its result with parse_mcp. help_entries feed the dynamic help message.

# graph/packs/builtin/hello/__init__.py
from graph.packs.protocols import Intent, Pack
from graph.tools import tool_invoke, parse_mcp


async def _say_hello(state: dict, data: dict) -> dict:
    raw = await tool_invoke("hello_say_hello", {"name": data.get("name")})
    payload = parse_mcp(raw)
    return {
        "status": "success",
        "message": payload.get("message", "Hello world!"),
        "origin_intent": "hello",
    }


PACK = Pack(
    id="hello",
    description="Demo pack: validates the pack loading and dispatch wiring.",
    help_entries=[
        "HELLO Renvoie un hello world (via le tool MCP)",
    ],
    intents=[
        Intent(
            name="hello",
            description="salutation simple : répondre via le tool MCP.",
            handler=_say_hello,
        ),
    ],
)

Declare it on the agent service:

KUZZLE_AGENT_PACKS=graph.packs.builtin.hello

At boot the agent logs:

[packs] loaded 'hello' from graph.packs.builtin.hello

Intent names must be unique across all agent packs, and tool names across all MCP packs; both registries reject collisions at boot. For multiple packs, separate the module paths with commas.

Step 5 — Verify the end-to-end flow #

With a valid Kuzzle token, the endpoint can be called directly:

curl -N -X POST http://localhost:7512/_/agent/run \
  -H "Authorization: Bearer $KUZZLE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [{ "content": "hello" }],
    "threadId": "test-1",
    "context": [
      { "description": "engineGroup", "value": "asset_tracking" },
      { "description": "engineIndex", "value": "tenant-asset_tracking-admin" }
    ]
  }'

The agent reads the last entry of messages as the user input (messages is required — an empty list returns 400 No messages received), threadId keys the conversation state, and context carries the AG-UI fields (engineGroup, engineIndex); soft-tenants are injected by the plugin from the authenticated user, so they are not passed here.

The response streams Server-Sent Events in the AG-UI format and ends with a final text message. Sending aide (or help) returns the built-in capabilities followed by the help_entries of every loaded pack.

Troubleshooting #

[packs] no pack declared — the agent (KUZZLE_AGENT_PACKS) or the MCP server (MCP_PACKS) booted with no pack. Set the relevant variable and recreate the container with docker compose up -d <service>. A plain docker restart does not re-read the .env file.