Behaviour-Runtime MCP Interface

kUML exposes its Behaviour-Runtime via the Model Context Protocol (MCP). This allows any MCP-compatible client — Claude Desktop, VS Code Copilot, or your own agent — to run kUML state machines and activity diagrams interactively, without writing a single line of integration code.

The MCP server is a standalone binary, kuml-mcp, built from the kuml-mcp module (application plugin, mainClass = dev.kuml.mcp.MainKt). It is not a subcommand of the kuml CLI — there is no kuml ai mcp-stdio command. Start it directly:

/path/to/kuml-mcp/bin/kuml-mcp

After ./gradlew :kuml-mcp:installDist the binary is at kuml-mcp/build/install/kuml-mcp/bin/kuml-mcp. Once kuml-mcp is bundled into runtimeZip (see the distribution changelog), brew install kuml puts it on PATH alongside kuml.

Claude Desktop (and other clients) connect to this process over stdio.

The five runtime tools

Tool Purpose Arguments Return shape

kuml.run.start

Start a new runtime session from a .kuml.kts file (or inline script). Auto-detects stm (state machine) vs act (activity).

{ "source": "<path-or-inline-script>", "kind"?: "stm"|"act", "element"?: "<diagram-name>" }

{ "sessionId": "rs-…​", "kind": "stm", "activeStates": [ …​ ], "trace": [] }

kuml.run.event

Send a named event to a running state-machine session.

{ "sessionId": "rs-…​", "event": "<event>", "payload"?: { …​ } }

{ "fired": [ "From→To" ], "activeStates": [ …​ ], "traceDelta": [ …​ ] }

kuml.run.snapshot

Non-destructively inspect a session: active states, variables, trace tail, step count.

{ "sessionId": "rs-…​" }

{ "activeStates": [ …​ ], "variables": { …​ }, "traceTail": [ last 20 entries ], "stepCount": <int> }

kuml.run.patch

Patch session variables and/or teleport the state machine to a named state.

{ "sessionId": "rs-…​", "variables"?: { …​ }, "forceState"?: "<state>" }

{ "ok": true, "activeStates": [ …​ ] }

kuml.run.stop

Terminate the session and return the full execution trace.

{ "sessionId": "rs-…​" }

{ "ok": true, "totalSteps": <int>, "traceLength": <int>, "trace": [ …​ ] }

sessionId is assigned by kuml.run.start (format rs-<hex>). All subsequent calls on the same session pass this ID. The server keeps sessions alive in memory until kuml.run.stop is called or the session expires (30 minutes of inactivity, configurable).

Claude Desktop configuration

Add the following block to your Claude Desktop claude_desktop_config.json (typically at ~/Library/Application\ Support/Claude/claude_desktop_config.json on macOS):

{
  "mcpServers": {
    "kuml": {
      "command": "/path/to/kuml-mcp/bin/kuml-mcp"
    }
  }
}

Restart Claude Desktop after editing the config. You should see kuml appear in the toolbox icon in the composer. kUML is now available as a first-class tool for every Claude Desktop conversation.

Full walkthrough: Pepela thermostat in Claude Desktop

This walkthrough drives the Pepela thermostat model through its full heating → idle → cooling → eco → idle → off cycle via MCP tool calls.

Step 1: Start the machine

// kuml.run.start
{
  "source": "kuml-examples/src/main/kotlin/dev/kuml/examples/pepela/pepela-thermostat-stm.kuml.kts"
}

Response:

{
  "sessionId": "rs-a1b2c3d4",
  "kind": "stm",
  "activeStates": ["Off"],
  "trace": []
}

Step 2: Fire powerOn

// kuml.run.event
{
  "sessionId": "rs-a1b2c3d4",
  "event": "powerOn"
}

Response:

{
  "fired": ["Off→Idle"],
  "activeStates": ["Idle"],
  "traceDelta": [
    { "type": "EventReceived",  "event": { "name": "powerOn" } },
    { "type": "Transitioned",   "trigger": "powerOn", "from": "Off", "to": "Idle" },
    { "type": "Entered",        "vertex": "Idle" }
  ]
}

Step 3: Fire a tick event to trigger heating

// kuml.run.event
{
  "sessionId": "rs-a1b2c3d4",
  "event": "tick",
  "payload": { "temperature": 16, "targetTemperature": 21 }
}

Response: activeStates: ["Heating"] — the guard event.temperature < event.targetTemperature - 1 evaluated to true (16 < 20).

Step 4: Snapshot without firing an event

// kuml.run.snapshot
{ "sessionId": "rs-a1b2c3d4" }

Response:

{
  "activeStates": ["Heating"],
  "variables": {},
  "traceTail": [ /* last 20 entries */ ],
  "stepCount": 2
}

Step 5: Complete the cycle and stop

Continue firing tick events with rising temperature to drive the machine through Idle → Cooling → Idle → Eco → Idle → Off, then stop:

// kuml.run.stop
{ "sessionId": "rs-a1b2c3d4" }

Response:

{
  "ok": true,
  "totalSteps": 6,
  "traceLength": 24,
  "trace": [ /* full trace */ ]
}
kuml.run.stop always returns "ok": true on a valid session ID; there is no separate terminated/non-terminated distinction in the response — inspect activeStates in the preceding kuml.run.snapshot/kuml.run.event calls to see whether the machine reached a final state.

Payload schema for guards

Guard expressions in the DSL use event.<key> to read payload fields. The MCP payload object maps directly to these keys — what you pass in kuml.run.event is what OCL sees:

{
  "sessionId": "rs-a1b2c3d4",
  "event": "tick",
  "payload": {
    "temperature": 19,
    "targetTemperature": 21,
    "humidity": 55
  }
}

A guard of event.temperature < event.targetTemperature - 1 receives 19 < 20 = true.

Using kuml.run.patch for guard testing

kuml.run.patch merges variables into the session’s context and/or forces the state machine to jump to a named state. This is intended for automated tests that need to reach a specific state without constructing a full event sequence:

// Force the overrideEco flag so the eco-mode guard fires immediately
{ "sessionId": "rs-a1b2c3d4", "variables": { "overrideEco": true } }
// Teleport the machine directly to the "Heating" state
{ "sessionId": "rs-a1b2c3d4", "forceState": "Heating" }

See also